diff --git a/build.gradle b/build.gradle index 3e636bd8b9..7eb4db25b0 100644 --- a/build.gradle +++ b/build.gradle @@ -594,6 +594,10 @@ dependencies { implementation 'com.nimbusds:nimbus-jose-jwt:9.48' implementation 'com.rfksystems:blake2b:2.0.0' implementation 'com.password4j:password4j:1.8.2' + + // Action privileges: check tables and compact collections + implementation 'com.selectivem.collections:special-collections-complete:1.4.0' + //JWT implementation "io.jsonwebtoken:jjwt-api:${jjwt_version}" implementation "io.jsonwebtoken:jjwt-impl:${jjwt_version}" @@ -747,6 +751,7 @@ dependencies { integrationTestImplementation "org.apache.httpcomponents:fluent-hc:4.5.14" integrationTestImplementation "org.apache.httpcomponents:httpcore:4.4.16" integrationTestImplementation "org.apache.httpcomponents:httpasyncclient:4.1.5" + integrationTestImplementation "org.mockito:mockito-core:5.14.2" //spotless implementation('com.google.googlejavaformat:google-java-format:1.25.2') { diff --git a/src/integrationTest/java/org/opensearch/security/SnapshotSteps.java b/src/integrationTest/java/org/opensearch/security/SnapshotSteps.java index a03891ecca..95af31f975 100644 --- a/src/integrationTest/java/org/opensearch/security/SnapshotSteps.java +++ b/src/integrationTest/java/org/opensearch/security/SnapshotSteps.java @@ -33,7 +33,7 @@ import static java.util.Objects.requireNonNull; import static org.opensearch.client.RequestOptions.DEFAULT; -class SnapshotSteps { +public class SnapshotSteps { private final SnapshotClient snapshotClient; diff --git a/src/integrationTest/java/org/opensearch/security/legacy/DoNotFailOnForbiddenTests.java b/src/integrationTest/java/org/opensearch/security/legacy/DoNotFailOnForbiddenTests.java new file mode 100644 index 0000000000..33bfe27cab --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/legacy/DoNotFailOnForbiddenTests.java @@ -0,0 +1,546 @@ +/* + * Copyright OpenSearch Contributors + * 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.security.legacy; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.hamcrest.Matchers; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.opensearch.action.fieldcaps.FieldCapabilitiesRequest; +import org.opensearch.action.fieldcaps.FieldCapabilitiesResponse; +import org.opensearch.action.get.MultiGetItemResponse; +import org.opensearch.action.get.MultiGetRequest; +import org.opensearch.action.get.MultiGetResponse; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.search.MultiSearchRequest; +import org.opensearch.action.search.MultiSearchResponse; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.action.search.SearchScrollRequest; +import org.opensearch.client.Client; +import org.opensearch.client.Request; +import org.opensearch.client.Response; +import org.opensearch.client.RestHighLevelClient; +import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.TestSecurityConfig.User; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +import static org.apache.http.HttpStatus.SC_CREATED; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.arrayContainingInAnyOrder; +import static org.hamcrest.Matchers.arrayWithSize; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions.Type.ADD; +import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; +import static org.opensearch.client.RequestOptions.DEFAULT; +import static org.opensearch.core.rest.RestStatus.FORBIDDEN; +import static org.opensearch.security.Song.FIELD_STARS; +import static org.opensearch.security.Song.FIELD_TITLE; +import static org.opensearch.security.Song.QUERY_TITLE_MAGNUM_OPUS; +import static org.opensearch.security.Song.QUERY_TITLE_NEXT_SONG; +import static org.opensearch.security.Song.QUERY_TITLE_POISON; +import static org.opensearch.security.Song.SONGS; +import static org.opensearch.security.Song.TITLE_MAGNUM_OPUS; +import static org.opensearch.security.Song.TITLE_NEXT_SONG; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; +import static org.opensearch.test.framework.cluster.SearchRequestFactory.averageAggregationRequest; +import static org.opensearch.test.framework.cluster.SearchRequestFactory.getSearchScrollRequest; +import static org.opensearch.test.framework.cluster.SearchRequestFactory.queryStringQueryRequest; +import static org.opensearch.test.framework.cluster.SearchRequestFactory.searchRequestWithScroll; +import static org.opensearch.test.framework.cluster.SearchRequestFactory.statsAggregationRequest; +import static org.opensearch.test.framework.matcher.ExceptionMatcherAssert.assertThatThrownBy; +import static org.opensearch.test.framework.matcher.GetResponseMatchers.containDocument; +import static org.opensearch.test.framework.matcher.GetResponseMatchers.containOnlyDocumentId; +import static org.opensearch.test.framework.matcher.GetResponseMatchers.documentContainField; +import static org.opensearch.test.framework.matcher.OpenSearchExceptionMatchers.statusException; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.containAggregationWithNameAndType; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.containNotEmptyScrollingId; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.isSuccessfulSearchResponse; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.numberOfHitsInPageIsEqualTo; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.numberOfTotalHitsIsEqualTo; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.searchHitContainsFieldWithValue; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.searchHitsContainDocumentWithId; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class DoNotFailOnForbiddenTests { + + /** + * Songs accessible for {@link #LIMITED_USER} + */ + private static final String MARVELOUS_SONGS = "marvelous_songs"; + + /** + * Songs inaccessible for {@link #LIMITED_USER} + */ + private static final String HORRIBLE_SONGS = "horrible_songs"; + + private static final String BOTH_INDEX_PATTERN = "*songs"; + + private static final String ID_1 = "1"; + private static final String ID_2 = "2"; + private static final String ID_3 = "3"; + private static final String ID_4 = "4"; + + private static final User ADMIN_USER = new User("admin").roles(ALL_ACCESS); + private static final User LIMITED_USER = new User("limited_user").roles( + new TestSecurityConfig.Role("limited-role").clusterPermissions( + "indices:data/read/mget", + "indices:data/read/msearch", + "indices:data/read/scroll", + "cluster:monitor/state", + "cluster:monitor/health", + "cluster:monitor/term" + ) + .indexPermissions( + "indices:data/read/search", + "indices:data/read/mget*", + "indices:data/read/field_caps", + "indices:data/read/field_caps*", + "indices:data/read/msearch", + "indices:data/read/scroll", + "indices:monitor/settings/get", + "indices:monitor/stats", + "indices:admin/aliases/get" + ) + .on(MARVELOUS_SONGS) + ); + + private static final User STATS_USER = new User("stats_user").roles( + new Role("test_role").clusterPermissions("cluster:monitor/*").indexPermissions("read", "indices:monitor/*").on("hi1") + ); + + private static final String BOTH_INDEX_ALIAS = "both-indices"; + private static final String FORBIDDEN_INDEX_ALIAS = "forbidden-index"; + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER, LIMITED_USER, STATS_USER) + .anonymousAuth(false) + .doNotFailOnForbidden(true) + .nodeSettings(Map.of(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), true)) + .build(); + + @BeforeClass + public static void createTestData() { + try (Client client = cluster.getInternalNodeClient()) { + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(MARVELOUS_SONGS).id(ID_1).source(SONGS[0].asMap())) + .actionGet(); + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(MARVELOUS_SONGS).id(ID_2).source(SONGS[1].asMap())) + .actionGet(); + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(MARVELOUS_SONGS).id(ID_3).source(SONGS[2].asMap())) + .actionGet(); + + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(HORRIBLE_SONGS).id(ID_4).source(SONGS[3].asMap())) + .actionGet(); + + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).indices(MARVELOUS_SONGS, HORRIBLE_SONGS).alias(BOTH_INDEX_ALIAS) + ) + ) + .actionGet(); + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).indices(HORRIBLE_SONGS).alias(FORBIDDEN_INDEX_ALIAS) + ) + ) + .actionGet(); + + } + } + + @Test + public void shouldPerformSimpleSearch_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = queryStringQueryRequest( + new String[] { MARVELOUS_SONGS, HORRIBLE_SONGS }, + QUERY_TITLE_MAGNUM_OPUS + ); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThatContainOneSong(searchResponse, ID_1, TITLE_MAGNUM_OPUS); + } + } + + private static void assertThatContainOneSong(SearchResponse searchResponse, String documentId, String title) { + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, MARVELOUS_SONGS, documentId)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, title)); + } + + @Test + public void shouldPerformSimpleSearch_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(HORRIBLE_SONGS, QUERY_TITLE_POISON); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldSearchForDocumentsViaIndexPattern_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(BOTH_INDEX_PATTERN, QUERY_TITLE_MAGNUM_OPUS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThatContainOneSong(searchResponse, ID_1, TITLE_MAGNUM_OPUS); + } + } + + @Test + public void shouldSearchForDocumentsViaIndexPattern_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(HORRIBLE_SONGS, QUERY_TITLE_POISON); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldSearchForDocumentsViaAlias_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(BOTH_INDEX_ALIAS, QUERY_TITLE_MAGNUM_OPUS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThatContainOneSong(searchResponse, ID_1, TITLE_MAGNUM_OPUS); + } + } + + @Test + public void shouldSearchForDocumentsViaAlias_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(FORBIDDEN_INDEX_ALIAS, QUERY_TITLE_POISON); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldSearchForDocumentsViaAll_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = queryStringQueryRequest("_all", QUERY_TITLE_MAGNUM_OPUS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThatContainOneSong(searchResponse, ID_1, TITLE_MAGNUM_OPUS); + } + } + + @Test + public void shouldSearchForDocumentsViaAll_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = queryStringQueryRequest("_all", QUERY_TITLE_POISON); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(0)); + } + } + + @Test + public void shouldMGetDocument_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + MultiGetRequest request = new MultiGetRequest().add(MARVELOUS_SONGS, ID_1).add(MARVELOUS_SONGS, ID_4); + + MultiGetResponse response = restHighLevelClient.mget(request, DEFAULT); + + MultiGetItemResponse[] responses = response.getResponses(); + assertThat(responses, arrayWithSize(2)); + MultiGetItemResponse firstResult = responses[0]; + MultiGetItemResponse secondResult = responses[1]; + assertThat(firstResult.getFailure(), nullValue()); + assertThat(secondResult.getFailure(), nullValue()); + assertThat( + firstResult.getResponse(), + allOf(containDocument(MARVELOUS_SONGS, ID_1), documentContainField(FIELD_TITLE, TITLE_MAGNUM_OPUS)) + ); + assertThat(secondResult.getResponse(), containOnlyDocumentId(MARVELOUS_SONGS, ID_4)); + } + } + + @Test + public void shouldMGetDocument_partial() throws Exception { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + MultiGetRequest request = new MultiGetRequest().add(MARVELOUS_SONGS, ID_1).add(HORRIBLE_SONGS, ID_4); + + MultiGetResponse response = restHighLevelClient.mget(request, DEFAULT); + + MultiGetItemResponse[] responses = response.getResponses(); + assertThat(responses, arrayWithSize(2)); + MultiGetItemResponse firstResult = responses[0]; + MultiGetItemResponse secondResult = responses[1]; + assertThat(firstResult.getFailure(), nullValue()); + assertThat( + firstResult.getResponse(), + allOf(containDocument(MARVELOUS_SONGS, ID_1), documentContainField(FIELD_TITLE, TITLE_MAGNUM_OPUS)) + ); + assertThat(secondResult.getFailure().getMessage(), containsString("no permissions for [indices:data/read/mget[shard]]")); + } + } + + @Test + public void shouldMGetDocument_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + MultiGetRequest request = new MultiGetRequest().add(HORRIBLE_SONGS, ID_4); + MultiGetResponse response = restHighLevelClient.mget(request, DEFAULT); + MultiGetItemResponse[] responses = response.getResponses(); + assertThat(responses, arrayWithSize(1)); + MultiGetItemResponse firstResult = responses[0]; + assertThat(firstResult.getFailure().getMessage(), containsString("no permissions for [indices:data/read/mget[shard]]")); + } + } + + @Test + public void shouldMSearchDocument_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + MultiSearchRequest request = new MultiSearchRequest(); + request.add(queryStringQueryRequest(BOTH_INDEX_PATTERN, QUERY_TITLE_MAGNUM_OPUS)); + request.add(queryStringQueryRequest(BOTH_INDEX_PATTERN, QUERY_TITLE_NEXT_SONG)); + + MultiSearchResponse response = restHighLevelClient.msearch(request, DEFAULT); + + MultiSearchResponse.Item[] responses = response.getResponses(); + assertThat(responses, Matchers.arrayWithSize(2)); + assertThat(responses[0].getFailure(), nullValue()); + assertThat(responses[1].getFailure(), nullValue()); + + assertThat(responses[0].getResponse(), searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + assertThat(responses[0].getResponse(), searchHitsContainDocumentWithId(0, MARVELOUS_SONGS, ID_1)); + assertThat(responses[1].getResponse(), searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_NEXT_SONG)); + assertThat(responses[1].getResponse(), searchHitsContainDocumentWithId(0, MARVELOUS_SONGS, ID_3)); + } + } + + @Test + public void shouldMSearchDocument_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + MultiSearchRequest request = new MultiSearchRequest(); + request.add(queryStringQueryRequest(FORBIDDEN_INDEX_ALIAS, QUERY_TITLE_POISON)); + MultiSearchResponse response = restHighLevelClient.msearch(request, DEFAULT); + MultiSearchResponse.Item[] responses = response.getResponses(); + assertThat(responses, Matchers.arrayWithSize(1)); + assertThat(responses[0].getFailure().getMessage(), containsString("no permissions for [indices:data/read/search]")); + } + } + + @Test + public void shouldGetFieldCapabilities_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().indices(MARVELOUS_SONGS, HORRIBLE_SONGS).fields(FIELD_TITLE); + + FieldCapabilitiesResponse response = restHighLevelClient.fieldCaps(request, DEFAULT); + + assertThat(response.get(), aMapWithSize(1)); + assertThat(response.getIndices(), arrayWithSize(1)); + assertThat(response.getField(FIELD_TITLE), hasKey("text")); + assertThat(response.getIndices(), arrayContainingInAnyOrder(MARVELOUS_SONGS)); + } + } + + @Test + public void shouldGetFieldCapabilities_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().indices(HORRIBLE_SONGS).fields(FIELD_TITLE); + + assertThatThrownBy(() -> restHighLevelClient.fieldCaps(request, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldScrollOverSearchResults_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = searchRequestWithScroll(BOTH_INDEX_PATTERN, 2); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containNotEmptyScrollingId()); + + SearchScrollRequest scrollRequest = getSearchScrollRequest(searchResponse); + + SearchResponse scrollResponse = restHighLevelClient.scroll(scrollRequest, DEFAULT); + assertThat(scrollResponse, isSuccessfulSearchResponse()); + assertThat(scrollResponse, containNotEmptyScrollingId()); + assertThat(scrollResponse, numberOfTotalHitsIsEqualTo(3)); + assertThat(scrollResponse, numberOfHitsInPageIsEqualTo(1)); + } + } + + @Test + public void shouldScrollOverSearchResults_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = searchRequestWithScroll(HORRIBLE_SONGS, 2); + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldPerformAggregation_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + final String aggregationName = "averageStars"; + SearchRequest searchRequest = averageAggregationRequest(BOTH_INDEX_PATTERN, aggregationName, FIELD_STARS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "avg")); + } + } + + @Test + public void shouldPerformAggregation_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + final String aggregationName = "averageStars"; + SearchRequest searchRequest = averageAggregationRequest(HORRIBLE_SONGS, aggregationName, FIELD_STARS); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldPerformStatAggregation_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + final String aggregationName = "statsStars"; + SearchRequest searchRequest = statsAggregationRequest(BOTH_INDEX_ALIAS, aggregationName, FIELD_STARS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "stats")); + } + } + + @Test + public void shouldPerformStatAggregation_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + final String aggregationName = "statsStars"; + SearchRequest searchRequest = statsAggregationRequest(HORRIBLE_SONGS, aggregationName, FIELD_STARS); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldPerformCatIndices_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + Request getIndicesRequest = new Request("GET", "/_cat/indices"); + // High level client doesn't support _cat/_indices API + Response getIndicesResponse = restHighLevelClient.getLowLevelClient().performRequest(getIndicesRequest); + List indexes = new BufferedReader(new InputStreamReader(getIndicesResponse.getEntity().getContent())).lines() + .collect(Collectors.toList()); + + assertThat(indexes.size(), equalTo(1)); + assertThat(indexes.get(0), containsString("marvelous_songs")); + } + } + + @Test + public void shouldPerformCatAliases_positive() throws IOException { + // DNFOF works for limited access user + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + Request getAliasesRequest = new Request("GET", "/_cat/aliases"); + Response getAliasesResponse = restHighLevelClient.getLowLevelClient().performRequest(getAliasesRequest); + List aliases = new BufferedReader(new InputStreamReader(getAliasesResponse.getEntity().getContent())).lines() + .collect(Collectors.toList()); + + // Does not fail on forbidden, but alias response only contains index which user has access to + assertThat(getAliasesResponse.getStatusLine().getStatusCode(), equalTo(200)); + assertThat(aliases.size(), equalTo(1)); + assertThat(aliases.get(0), containsString("marvelous_songs")); + assertThat(aliases.get(0), not(containsString("horrible_songs"))); + + } + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { + Request getAliasesRequest = new Request("GET", "/_cat/aliases"); + Response getAliasesResponse = restHighLevelClient.getLowLevelClient().performRequest(getAliasesRequest); + List aliases = new BufferedReader(new InputStreamReader(getAliasesResponse.getEntity().getContent())).lines() + .collect(Collectors.toList()); + + // Admin has access to all + assertThat(getAliasesResponse.getStatusLine().getStatusCode(), equalTo(200)); + // Aliases have one entry for each index + // This response is [(both-indices: marvelous_songs), (both-indices: horrible_songs), (forbidden-index: horrible_songs)] + assertThat(aliases.size(), equalTo(3)); + assertThat(aliases, hasItem(containsString("marvelous_songs"))); + assertThat(aliases, hasItem(containsString("horrible_songs"))); + + } + } + + @Test + public void checkStatsApi() { + // As admin creates 2 documents in different indices, can find both indices in search, cat indice & stats APIs + try (final TestRestClient client = cluster.getRestClient(ADMIN_USER.getName(), ADMIN_USER.getPassword())) { + final HttpResponse createDoc1 = client.postJson("hi1/_doc?refresh=true", "{\"hi\":\"Hello1\"}"); + createDoc1.assertStatusCode(SC_CREATED); + final HttpResponse createDoc2 = client.postJson("hi2/_doc?refresh=true", "{\"hi\":\"Hello2\"}"); + createDoc2.assertStatusCode(SC_CREATED); + + final HttpResponse search = client.postJson("hi*/_search", "{}"); + assertThat("Unexpected document results in search:" + search.getBody(), search.getBody(), containsString("2")); + + final HttpResponse catIndices = client.get("_cat/indices"); + assertThat("Expected cat indices: " + catIndices.getBody(), catIndices.getBody(), containsString("hi1")); + assertThat("Expected cat indices: " + catIndices.getBody(), catIndices.getBody(), containsString("hi2")); + + final HttpResponse stats = client.get("hi*/_stats?filter_path=indices.*.uuid"); + assertThat("Expected stats indices: " + stats.getBody(), stats.getBody(), containsString("hi1")); + assertThat("Expected stats indices: " + stats.getBody(), stats.getBody(), containsString("hi2")); + } + + // As user who can only see the index "hi1" make sure that DNFOF is filtering out "hi2" + try (final TestRestClient client = cluster.getRestClient(STATS_USER.getName(), STATS_USER.getPassword())) { + final HttpResponse search = client.postJson("hi*/_search", "{}"); + assertThat("Unexpected document results in search:" + search.getBody(), search.getBody(), containsString("1")); + + final HttpResponse catIndices = client.get("_cat/indices"); + assertThat("Expected cat indices: " + catIndices.getBody(), catIndices.getBody(), containsString("hi1")); + assertThat("Unexpected cat indices: " + catIndices.getBody(), catIndices.getBody(), not(containsString("hi2"))); + + final HttpResponse stats = client.get("hi*/_stats?filter_path=indices.*.uuid"); + assertThat("Expected stats indices: " + stats.getBody(), stats.getBody(), containsString("hi1")); + assertThat("Unexpected stats indices: " + stats.getBody(), stats.getBody(), not(containsString("hi2"))); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/legacy/PointInTimeOperationTest.java b/src/integrationTest/java/org/opensearch/security/legacy/PointInTimeOperationTest.java new file mode 100644 index 0000000000..22ddc654ba --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/legacy/PointInTimeOperationTest.java @@ -0,0 +1,417 @@ +/* +* Copyright OpenSearch Contributors +* 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.security.legacy; + +import java.io.IOException; +import java.util.Map; + +import com.carrotsearch.randomizedtesting.RandomizedRunner; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.search.CreatePitRequest; +import org.opensearch.action.search.CreatePitResponse; +import org.opensearch.action.search.DeletePitRequest; +import org.opensearch.action.search.DeletePitResponse; +import org.opensearch.action.search.GetAllPitNodesResponse; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.client.Client; +import org.opensearch.client.RestHighLevelClient; +import org.opensearch.common.unit.TimeValue; +import org.opensearch.search.builder.PointInTimeBuilder; +import org.opensearch.search.builder.SearchSourceBuilder; +import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions.Type.ADD; +import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; +import static org.opensearch.client.RequestOptions.DEFAULT; +import static org.opensearch.core.rest.RestStatus.FORBIDDEN; +import static org.opensearch.core.rest.RestStatus.OK; +import static org.opensearch.security.Song.SONGS; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; +import static org.opensearch.test.framework.matcher.ExceptionMatcherAssert.assertThatThrownBy; +import static org.opensearch.test.framework.matcher.OpenSearchExceptionMatchers.statusException; +import static org.opensearch.test.framework.matcher.PitResponseMatchers.deleteResponseContainsExactlyPitWithIds; +import static org.opensearch.test.framework.matcher.PitResponseMatchers.getAllResponseContainsExactlyPitWithIds; +import static org.opensearch.test.framework.matcher.PitResponseMatchers.isSuccessfulCreatePitResponse; +import static org.opensearch.test.framework.matcher.PitResponseMatchers.isSuccessfulDeletePitResponse; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.isSuccessfulSearchResponse; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.searchHitsContainDocumentsInAnyOrder; + +@RunWith(RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class PointInTimeOperationTest { + + private static final String FIRST_SONG_INDEX = "song-index-1"; + private static final String FIRST_INDEX_ALIAS = "song-index-1-alias"; + private static final String SECOND_SONG_INDEX = "song-index-2"; + private static final String SECOND_INDEX_ALIAS = "song-index-2-alias"; + + private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + /** + * User who is allowed to perform PIT operations only on the {@link #FIRST_SONG_INDEX} + */ + private static final TestSecurityConfig.User LIMITED_POINT_IN_TIME_USER = new TestSecurityConfig.User("limited_point_in_time_user") + .roles( + new TestSecurityConfig.Role("limited_point_in_time_user").indexPermissions( + "indices:data/read/point_in_time/create", + "indices:data/read/point_in_time/delete", + "indices:data/read/search", + "indices:data/read/point_in_time/readall", // anyway user needs the all indexes permission (*) to find all pits + "indices:monitor/point_in_time/segments" // anyway user needs the all indexes permission (*) to list all pits segments + ).on(FIRST_SONG_INDEX) + ); + /** + * User who is allowed to perform PIT operations on all indices + */ + private static final TestSecurityConfig.User POINT_IN_TIME_USER = new TestSecurityConfig.User("point_in_time_user").roles( + new TestSecurityConfig.Role("point_in_time_user").indexPermissions( + "indices:data/read/point_in_time/create", + "indices:data/read/point_in_time/delete", + "indices:data/read/search", + "indices:data/read/point_in_time/readall", + "indices:monitor/point_in_time/segments" + ).on("*") + ); + + private static final String ID_1 = "1"; + private static final String ID_2 = "2"; + private static final String ID_3 = "3"; + private static final String ID_4 = "4"; + + @BeforeClass + public static void createTestData() { + try (Client client = cluster.getInternalNodeClient()) { + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(FIRST_SONG_INDEX).id(ID_1).source(SONGS[0].asMap())) + .actionGet(); + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(FIRST_SONG_INDEX).id(ID_2).source(SONGS[1].asMap())) + .actionGet(); + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(FIRST_SONG_INDEX).id(ID_3).source(SONGS[2].asMap())) + .actionGet(); + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).indices(FIRST_SONG_INDEX).alias(FIRST_INDEX_ALIAS) + ) + ) + .actionGet(); + + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(SECOND_SONG_INDEX).id(ID_4).source(SONGS[3].asMap())) + .actionGet(); + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).indices(SECOND_SONG_INDEX).alias(SECOND_INDEX_ALIAS) + ) + ) + .actionGet(); + } + } + + @Before + public void cleanUpPits() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { + restHighLevelClient.deleteAllPits(DEFAULT); + } + } + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .anonymousAuth(false) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER, LIMITED_POINT_IN_TIME_USER, POINT_IN_TIME_USER) + .nodeSettings(Map.of(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), true)) + .build(); + + @Test + public void createPit_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + CreatePitRequest createPitRequest = new CreatePitRequest(TimeValue.timeValueMinutes(30), false, FIRST_SONG_INDEX); + + CreatePitResponse createPitResponse = restHighLevelClient.createPit(createPitRequest, DEFAULT); + + assertThat(createPitResponse, isSuccessfulCreatePitResponse()); + } + } + + @Test + public void createPitWithIndexAlias_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + CreatePitRequest createPitRequest = new CreatePitRequest(TimeValue.timeValueMinutes(30), false, FIRST_INDEX_ALIAS); + + CreatePitResponse createPitResponse = restHighLevelClient.createPit(createPitRequest, DEFAULT); + + assertThat(createPitResponse, isSuccessfulCreatePitResponse()); + } + } + + @Test + public void createPit_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + CreatePitRequest createPitRequest = new CreatePitRequest(TimeValue.timeValueMinutes(30), false, SECOND_SONG_INDEX); + + assertThatThrownBy(() -> restHighLevelClient.createPit(createPitRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void createPitWithIndexAlias_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + CreatePitRequest createPitRequest = new CreatePitRequest(TimeValue.timeValueMinutes(30), false, SECOND_INDEX_ALIAS); + + assertThatThrownBy(() -> restHighLevelClient.createPit(createPitRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void listAllPits_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(POINT_IN_TIME_USER)) { + String firstIndexPit = createPitForIndices(FIRST_SONG_INDEX); + String secondIndexPit = createPitForIndices(SECOND_SONG_INDEX); + + GetAllPitNodesResponse getAllPitsResponse = restHighLevelClient.getAllPits(DEFAULT); + + assertThat(getAllPitsResponse, getAllResponseContainsExactlyPitWithIds(firstIndexPit, secondIndexPit)); + } + } + + @Test + public void listAllPits_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + assertThatThrownBy(() -> restHighLevelClient.getAllPits(DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void deletePit_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(FIRST_SONG_INDEX); + + DeletePitResponse deletePitResponse = restHighLevelClient.deletePit(new DeletePitRequest(existingPitId), DEFAULT); + assertThat(deletePitResponse, isSuccessfulDeletePitResponse()); + assertThat(deletePitResponse, deleteResponseContainsExactlyPitWithIds(existingPitId)); + } + } + + @Test + public void deletePitCreatedWithIndexAlias_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(FIRST_INDEX_ALIAS); + + DeletePitResponse deletePitResponse = restHighLevelClient.deletePit(new DeletePitRequest(existingPitId), DEFAULT); + assertThat(deletePitResponse, isSuccessfulDeletePitResponse()); + assertThat(deletePitResponse, deleteResponseContainsExactlyPitWithIds(existingPitId)); + } + } + + @Test + public void deletePit_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(SECOND_SONG_INDEX); + + assertThatThrownBy( + () -> restHighLevelClient.deletePit(new DeletePitRequest(existingPitId), DEFAULT), + statusException(FORBIDDEN) + ); + } + } + + @Test + public void deletePitCreatedWithIndexAlias_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(SECOND_INDEX_ALIAS); + + assertThatThrownBy( + () -> restHighLevelClient.deletePit(new DeletePitRequest(existingPitId), DEFAULT), + statusException(FORBIDDEN) + ); + } + } + + @Test + public void deleteAllPits_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(POINT_IN_TIME_USER)) { + String firstIndexPit = createPitForIndices(FIRST_SONG_INDEX); + String secondIndexPit = createPitForIndices(SECOND_SONG_INDEX); + + DeletePitResponse deletePitResponse = restHighLevelClient.deleteAllPits(DEFAULT); + assertThat(deletePitResponse, isSuccessfulDeletePitResponse()); + assertThat(deletePitResponse, deleteResponseContainsExactlyPitWithIds(firstIndexPit, secondIndexPit)); + } + } + + @Test + public void deleteAllPits_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + assertThatThrownBy(() -> restHighLevelClient.deleteAllPits(DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void searchWithPit_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(FIRST_SONG_INDEX); + + SearchRequest searchRequest = new SearchRequest(); + searchRequest.source(new SearchSourceBuilder().pointInTimeBuilder(new PointInTimeBuilder(existingPitId))); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat( + searchResponse, + searchHitsContainDocumentsInAnyOrder( + Pair.of(FIRST_SONG_INDEX, ID_1), + Pair.of(FIRST_SONG_INDEX, ID_2), + Pair.of(FIRST_SONG_INDEX, ID_3) + ) + ); + } + } + + @Test + public void searchWithPitCreatedWithIndexAlias_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(FIRST_INDEX_ALIAS); + + SearchRequest searchRequest = new SearchRequest(); + searchRequest.source(new SearchSourceBuilder().pointInTimeBuilder(new PointInTimeBuilder(existingPitId))); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat( + searchResponse, + searchHitsContainDocumentsInAnyOrder( + Pair.of(FIRST_SONG_INDEX, ID_1), + Pair.of(FIRST_SONG_INDEX, ID_2), + Pair.of(FIRST_SONG_INDEX, ID_3) + ) + ); + } + } + + @Test + public void searchWithPit_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(SECOND_SONG_INDEX); + + SearchRequest searchRequest = new SearchRequest(); + searchRequest.source(new SearchSourceBuilder().pointInTimeBuilder(new PointInTimeBuilder(existingPitId))); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void searchWithPitCreatedWithIndexAlias_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(SECOND_INDEX_ALIAS); + + SearchRequest searchRequest = new SearchRequest(); + searchRequest.source(new SearchSourceBuilder().pointInTimeBuilder(new PointInTimeBuilder(existingPitId))); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void listPitSegments_positive() throws IOException { + try (TestRestClient restClient = cluster.getRestClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(FIRST_SONG_INDEX); + String body = String.format("{\"pit_id\":[\"%s\"]}", existingPitId); + HttpResponse response = restClient.getWithJsonBody("/_cat/pit_segments", body); + + response.assertStatusCode(OK.getStatus()); + } + } + + @Test + public void listPitSegmentsCreatedWithIndexAlias_positive() throws IOException { + try (TestRestClient restClient = cluster.getRestClient(POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(FIRST_INDEX_ALIAS); + String body = String.format("{\"pit_id\":[\"%s\"]}", existingPitId); + HttpResponse response = restClient.getWithJsonBody("/_cat/pit_segments", body); + + response.assertStatusCode(OK.getStatus()); + } + } + + @Test + public void listPitSegments_negative() throws IOException { + try (TestRestClient restClient = cluster.getRestClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(SECOND_SONG_INDEX); + String body = String.format("{\"pit_id\":[\"%s\"]}", existingPitId); + HttpResponse response = restClient.getWithJsonBody("/_cat/pit_segments", body); + + response.assertStatusCode(FORBIDDEN.getStatus()); + } + } + + @Test + public void listPitSegmentsCreatedWithIndexAlias_negative() throws IOException { + try (TestRestClient restClient = cluster.getRestClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(SECOND_INDEX_ALIAS); + String body = String.format("{\"pit_id\":[\"%s\"]}", existingPitId); + HttpResponse response = restClient.getWithJsonBody("/_cat/pit_segments", body); + + response.assertStatusCode(FORBIDDEN.getStatus()); + } + } + + @Test + public void listAllPitSegments_positive() { + try (TestRestClient restClient = cluster.getRestClient(POINT_IN_TIME_USER)) { + HttpResponse response = restClient.get("_cat/pit_segments/_all"); + + response.assertStatusCode(OK.getStatus()); + } + } + + @Test + public void listAllPitSegments_negative() { + try (TestRestClient restClient = cluster.getRestClient(LIMITED_POINT_IN_TIME_USER)) { + HttpResponse response = restClient.get("_cat/pit_segments/_all"); + + response.assertStatusCode(FORBIDDEN.getStatus()); + } + } + + /** + * Creates PIT for given indices. Returns PIT id. + */ + private String createPitForIndices(String... indices) throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { + CreatePitRequest createPitRequest = new CreatePitRequest(TimeValue.timeValueMinutes(30), false, indices); + + CreatePitResponse createPitResponse = restHighLevelClient.createPit(createPitRequest, DEFAULT); + + assertThat(createPitResponse, isSuccessfulCreatePitResponse()); + return createPitResponse.getId(); + } + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/legacy/SearchOperationTest.java b/src/integrationTest/java/org/opensearch/security/legacy/SearchOperationTest.java new file mode 100644 index 0000000000..b3aec598ec --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/legacy/SearchOperationTest.java @@ -0,0 +1,2761 @@ +/* +* Copyright OpenSearch Contributors +* 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.security.legacy; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import com.google.common.base.Stopwatch; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.awaitility.Awaitility; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.action.admin.cluster.health.ClusterHealthRequest; +import org.opensearch.action.admin.cluster.health.ClusterHealthResponse; +import org.opensearch.action.admin.cluster.repositories.delete.DeleteRepositoryRequest; +import org.opensearch.action.admin.cluster.repositories.put.PutRepositoryRequest; +import org.opensearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse; +import org.opensearch.action.admin.indices.alias.Alias; +import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; +import org.opensearch.action.admin.indices.cache.clear.ClearIndicesCacheRequest; +import org.opensearch.action.admin.indices.cache.clear.ClearIndicesCacheResponse; +import org.opensearch.action.admin.indices.delete.DeleteIndexRequest; +import org.opensearch.action.admin.indices.exists.indices.IndicesExistsRequest; +import org.opensearch.action.admin.indices.open.OpenIndexRequest; +import org.opensearch.action.admin.indices.open.OpenIndexResponse; +import org.opensearch.action.admin.indices.settings.get.GetSettingsRequest; +import org.opensearch.action.admin.indices.settings.get.GetSettingsResponse; +import org.opensearch.action.admin.indices.settings.put.UpdateSettingsRequest; +import org.opensearch.action.admin.indices.template.delete.DeleteIndexTemplateRequest; +import org.opensearch.action.admin.indices.template.get.GetIndexTemplatesRequest; +import org.opensearch.action.admin.indices.template.get.GetIndexTemplatesResponse; +import org.opensearch.action.bulk.BulkRequest; +import org.opensearch.action.bulk.BulkResponse; +import org.opensearch.action.delete.DeleteRequest; +import org.opensearch.action.delete.DeleteResponse; +import org.opensearch.action.fieldcaps.FieldCapabilitiesRequest; +import org.opensearch.action.fieldcaps.FieldCapabilitiesResponse; +import org.opensearch.action.get.GetRequest; +import org.opensearch.action.get.GetResponse; +import org.opensearch.action.get.MultiGetItemResponse; +import org.opensearch.action.get.MultiGetRequest; +import org.opensearch.action.get.MultiGetRequest.Item; +import org.opensearch.action.get.MultiGetResponse; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.search.MultiSearchRequest; +import org.opensearch.action.search.MultiSearchResponse; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.action.search.SearchScrollRequest; +import org.opensearch.action.update.UpdateRequest; +import org.opensearch.action.update.UpdateResponse; +import org.opensearch.client.Client; +import org.opensearch.client.ClusterAdminClient; +import org.opensearch.client.IndicesAdminClient; +import org.opensearch.client.RestHighLevelClient; +import org.opensearch.client.core.CountRequest; +import org.opensearch.client.indices.CloseIndexRequest; +import org.opensearch.client.indices.CloseIndexResponse; +import org.opensearch.client.indices.CreateIndexRequest; +import org.opensearch.client.indices.CreateIndexResponse; +import org.opensearch.client.indices.GetIndexRequest; +import org.opensearch.client.indices.GetIndexResponse; +import org.opensearch.client.indices.GetMappingsRequest; +import org.opensearch.client.indices.GetMappingsResponse; +import org.opensearch.client.indices.PutIndexTemplateRequest; +import org.opensearch.client.indices.PutMappingRequest; +import org.opensearch.client.indices.ResizeRequest; +import org.opensearch.client.indices.ResizeResponse; +import org.opensearch.cluster.health.ClusterHealthStatus; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.metadata.IndexTemplateMetadata; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.MatchQueryBuilder; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.index.reindex.BulkByScrollResponse; +import org.opensearch.index.reindex.ReindexRequest; +import org.opensearch.repositories.RepositoryMissingException; +import org.opensearch.search.builder.SearchSourceBuilder; +import org.opensearch.security.IndexOperationsHelper; +import org.opensearch.security.SnapshotSteps; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.test.framework.AuditCompliance; +import org.opensearch.test.framework.AuditConfiguration; +import org.opensearch.test.framework.AuditFilters; +import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.TestSecurityConfig.User; +import org.opensearch.test.framework.audit.AuditLogsRule; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions.Type.ADD; +import static org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions.Type.REMOVE; +import static org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions.Type.REMOVE_INDEX; +import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; +import static org.opensearch.client.RequestOptions.DEFAULT; +import static org.opensearch.core.rest.RestStatus.ACCEPTED; +import static org.opensearch.core.rest.RestStatus.BAD_REQUEST; +import static org.opensearch.core.rest.RestStatus.FORBIDDEN; +import static org.opensearch.core.rest.RestStatus.INTERNAL_SERVER_ERROR; +import static org.opensearch.rest.RestRequest.Method.DELETE; +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.rest.RestRequest.Method.PUT; +import static org.opensearch.security.Song.FIELD_ARTIST; +import static org.opensearch.security.Song.FIELD_STARS; +import static org.opensearch.security.Song.FIELD_TITLE; +import static org.opensearch.security.Song.QUERY_TITLE_MAGNUM_OPUS; +import static org.opensearch.security.Song.QUERY_TITLE_NEXT_SONG; +import static org.opensearch.security.Song.QUERY_TITLE_POISON; +import static org.opensearch.security.Song.SONGS; +import static org.opensearch.security.Song.TITLE_MAGNUM_OPUS; +import static org.opensearch.security.Song.TITLE_NEXT_SONG; +import static org.opensearch.security.Song.TITLE_POISON; +import static org.opensearch.security.Song.TITLE_SONG_1_PLUS_1; +import static org.opensearch.security.auditlog.impl.AuditCategory.INDEX_EVENT; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; +import static org.opensearch.test.framework.audit.AuditMessagePredicate.auditPredicate; +import static org.opensearch.test.framework.audit.AuditMessagePredicate.grantedPrivilege; +import static org.opensearch.test.framework.audit.AuditMessagePredicate.missingPrivilege; +import static org.opensearch.test.framework.audit.AuditMessagePredicate.userAuthenticated; +import static org.opensearch.test.framework.cluster.SearchRequestFactory.averageAggregationRequest; +import static org.opensearch.test.framework.cluster.SearchRequestFactory.getSearchScrollRequest; +import static org.opensearch.test.framework.cluster.SearchRequestFactory.queryStringQueryRequest; +import static org.opensearch.test.framework.cluster.SearchRequestFactory.searchRequestWithScroll; +import static org.opensearch.test.framework.cluster.SearchRequestFactory.statsAggregationRequest; +import static org.opensearch.test.framework.matcher.BulkResponseMatchers.bulkResponseContainExceptions; +import static org.opensearch.test.framework.matcher.BulkResponseMatchers.failureBulkResponse; +import static org.opensearch.test.framework.matcher.BulkResponseMatchers.successBulkResponse; +import static org.opensearch.test.framework.matcher.ClusterMatchers.aliasExists; +import static org.opensearch.test.framework.matcher.ClusterMatchers.clusterContainSuccessSnapshot; +import static org.opensearch.test.framework.matcher.ClusterMatchers.clusterContainTemplate; +import static org.opensearch.test.framework.matcher.ClusterMatchers.clusterContainTemplateWithAlias; +import static org.opensearch.test.framework.matcher.ClusterMatchers.clusterContainsDocument; +import static org.opensearch.test.framework.matcher.ClusterMatchers.clusterContainsDocumentWithFieldValue; +import static org.opensearch.test.framework.matcher.ClusterMatchers.clusterContainsSnapshotRepository; +import static org.opensearch.test.framework.matcher.ClusterMatchers.indexExists; +import static org.opensearch.test.framework.matcher.ClusterMatchers.indexMappingIsEqualTo; +import static org.opensearch.test.framework.matcher.ClusterMatchers.indexSettingsContainValues; +import static org.opensearch.test.framework.matcher.ClusterMatchers.indexStateIsEqualTo; +import static org.opensearch.test.framework.matcher.ClusterMatchers.snapshotInClusterDoesNotExists; +import static org.opensearch.test.framework.matcher.DeleteResponseMatchers.isSuccessfulDeleteResponse; +import static org.opensearch.test.framework.matcher.ExceptionMatcherAssert.assertThatThrownBy; +import static org.opensearch.test.framework.matcher.FieldCapabilitiesResponseMatchers.containsExactlyIndices; +import static org.opensearch.test.framework.matcher.FieldCapabilitiesResponseMatchers.containsFieldWithNameAndType; +import static org.opensearch.test.framework.matcher.FieldCapabilitiesResponseMatchers.numberOfFieldsIsEqualTo; +import static org.opensearch.test.framework.matcher.GetResponseMatchers.containDocument; +import static org.opensearch.test.framework.matcher.GetResponseMatchers.documentContainField; +import static org.opensearch.test.framework.matcher.IndexResponseMatchers.getIndexResponseContainsIndices; +import static org.opensearch.test.framework.matcher.IndexResponseMatchers.getMappingsResponseContainsIndices; +import static org.opensearch.test.framework.matcher.IndexResponseMatchers.getSettingsResponseContainsIndices; +import static org.opensearch.test.framework.matcher.IndexResponseMatchers.isSuccessfulClearIndicesCacheResponse; +import static org.opensearch.test.framework.matcher.IndexResponseMatchers.isSuccessfulCloseIndexResponse; +import static org.opensearch.test.framework.matcher.IndexResponseMatchers.isSuccessfulCreateIndexResponse; +import static org.opensearch.test.framework.matcher.IndexResponseMatchers.isSuccessfulOpenIndexResponse; +import static org.opensearch.test.framework.matcher.IndexResponseMatchers.isSuccessfulResizeResponse; +import static org.opensearch.test.framework.matcher.MultiGetResponseMatchers.isSuccessfulMultiGetResponse; +import static org.opensearch.test.framework.matcher.MultiGetResponseMatchers.numberOfGetItemResponsesIsEqualTo; +import static org.opensearch.test.framework.matcher.MultiSearchResponseMatchers.isSuccessfulMultiSearchResponse; +import static org.opensearch.test.framework.matcher.MultiSearchResponseMatchers.numberOfSearchItemResponsesIsEqualTo; +import static org.opensearch.test.framework.matcher.OpenSearchExceptionMatchers.errorMessageContain; +import static org.opensearch.test.framework.matcher.OpenSearchExceptionMatchers.statusException; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.containAggregationWithNameAndType; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.containNotEmptyScrollingId; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.isSuccessfulSearchResponse; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.numberOfHitsInPageIsEqualTo; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.numberOfTotalHitsIsEqualTo; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.searchHitContainsFieldWithValue; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.searchHitsContainDocumentWithId; +import static org.opensearch.test.framework.matcher.UpdateResponseMatchers.isSuccessfulUpdateResponse; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class SearchOperationTest { + + private static final Logger log = LogManager.getLogger(SearchOperationTest.class); + + public static final String SONG_INDEX_NAME = "song_lyrics"; + public static final String PROHIBITED_SONG_INDEX_NAME = "prohibited_song_lyrics"; + public static final String WRITE_SONG_INDEX_NAME = "write_song_index"; + + public static final String SONG_LYRICS_ALIAS = "song_lyrics_index_alias"; + public static final String PROHIBITED_SONG_ALIAS = "prohibited_song_lyrics_index_alias"; + private static final String COLLECTIVE_INDEX_ALIAS = "collective-index-alias"; + private static final String TEMPLATE_INDEX_PREFIX = "song-transcription*"; + public static final String TEMPORARY_ALIAS_NAME = "temporary-alias"; + public static final String ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001 = "alias-used-in-musical-index-template-0001"; + public static final String ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002 = "alias-used-in-musical-index-template-0002"; + public static final String ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0003 = "alias-used-in-musical-index-template-0003"; + public static final String INDEX_NAME_SONG_TRANSCRIPTION_JAZZ = "song-transcription-jazz"; + + public static final String MUSICAL_INDEX_TEMPLATE = "musical-index-template"; + public static final String ALIAS_CREATE_INDEX_WITH_ALIAS_POSITIVE = "alias_create_index_with_alias_positive"; + public static final String ALIAS_CREATE_INDEX_WITH_ALIAS_NEGATIVE = "alias_create_index_with_alias_negative"; + + public static final String UNDELETABLE_TEMPLATE_NAME = "undeletable-template-name"; + + public static final String ALIAS_FROM_UNDELETABLE_TEMPLATE = "alias-from-undeletable-template"; + + public static final String TEST_SNAPSHOT_REPOSITORY_NAME = "test-snapshot-repository"; + + public static final String UNUSED_SNAPSHOT_REPOSITORY_NAME = "unused-snapshot-repository"; + + public static final String RESTORED_SONG_INDEX_NAME = "restored_" + WRITE_SONG_INDEX_NAME; + + public static final String UPDATE_DELETE_OPERATION_INDEX_NAME = "update_delete_index"; + + public static final String DOCUMENT_TO_UPDATE_ID = "doc_to_update"; + + private static final String ID_P4 = "4"; + private static final String ID_S3 = "3"; + private static final String ID_S2 = "2"; + private static final String ID_S1 = "1"; + + static final User ADMIN_USER = new User("admin").roles(ALL_ACCESS); + + /** + * All user read permissions are related to {@link #SONG_INDEX_NAME} index + */ + static final User LIMITED_READ_USER = new User("limited_read_user").roles( + new Role("limited-song-reader").clusterPermissions( + "indices:data/read/mget", + "indices:data/read/msearch", + "indices:data/read/scroll" + ) + .indexPermissions( + "indices:data/read/search", + "indices:data/read/get", + "indices:data/read/mget*", + "indices:admin/aliases", + "indices:data/read/field_caps", + "indices:data/read/field_caps*" + ) + .on(SONG_INDEX_NAME) + ); + + static final User LIMITED_WRITE_USER = new User("limited_write_user").roles( + new Role("limited-write-role").clusterPermissions( + "indices:data/write/bulk", + "indices:admin/template/put", + "indices:admin/template/delete", + "cluster:admin/repository/put", + "cluster:admin/repository/delete", + "cluster:admin/snapshot/create", + "cluster:admin/snapshot/status", + "cluster:admin/snapshot/status[nodes]", + "cluster:admin/snapshot/delete", + "cluster:admin/snapshot/get", + "cluster:admin/snapshot/restore" + ) + .indexPermissions( + "indices:data/write/index", + "indices:data/write/bulk[s]", + "indices:admin/create", + "indices:admin/mapping/put", + "indices:data/write/update", + "indices:data/write/bulk[s]", + "indices:data/write/delete", + "indices:data/write/bulk[s]" + ) + .on(WRITE_SONG_INDEX_NAME), + new Role("transcription-role").indexPermissions( + "indices:data/write/index", + "indices:admin/create", + "indices:data/write/bulk[s]", + "indices:admin/mapping/put" + ).on(INDEX_NAME_SONG_TRANSCRIPTION_JAZZ), + new Role("limited-write-index-restore-role").indexPermissions( + "indices:data/write/index", + "indices:admin/create", + "indices:data/read/search" + ).on(RESTORED_SONG_INDEX_NAME) + ); + + /** + * User who is allowed read both index {@link #SONG_INDEX_NAME} and {@link #PROHIBITED_SONG_INDEX_NAME} + */ + static final User DOUBLE_READER_USER = new User("double_read_user").roles( + new Role("full-song-reader").indexPermissions("indices:data/read/search").on(SONG_INDEX_NAME, PROHIBITED_SONG_INDEX_NAME) + ); + + static final User REINDEXING_USER = new User("reindexing_user").roles( + new Role("song-reindexing-target-write").clusterPermissions("indices:data/write/reindex", "indices:data/write/bulk") + .indexPermissions("indices:admin/create", "indices:data/write/index", "indices:data/write/bulk[s]", "indices:admin/mapping/put") + .on(WRITE_SONG_INDEX_NAME), + new Role("song-reindexing-source-read").clusterPermissions("indices:data/read/scroll") + .indexPermissions("indices:data/read/search") + .on(SONG_INDEX_NAME) + ); + + private Client internalClient; + /** + * User who is allowed to update and delete documents on index {@link #UPDATE_DELETE_OPERATION_INDEX_NAME} + */ + static final User UPDATE_DELETE_USER = new User("update_delete_user").roles( + new Role("document-updater").clusterPermissions("indices:data/write/bulk") + .indexPermissions( + "indices:data/write/update", + "indices:data/write/index", + "indices:data/write/bulk[s]", + "indices:admin/mapping/put" + ) + .on(UPDATE_DELETE_OPERATION_INDEX_NAME), + new Role("document-remover").indexPermissions("indices:data/write/delete").on(UPDATE_DELETE_OPERATION_INDEX_NAME) + ); + + static final String INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX = "index_operations_"; + + /** + * User who is allowed to perform index-related operations on + * indices with names prefixed by the {@link #INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX} + */ + static final User USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES = new User("index-operation-tester").roles( + new Role("index-manager").clusterPermissions("cluster:monitor/health") + .indexPermissions( + "indices:admin/create", + "indices:admin/get", + "indices:admin/delete", + "indices:admin/close", + "indices:admin/close*", + "indices:admin/open", + "indices:admin/resize", + "indices:monitor/stats", + "indices:monitor/settings/get", + "indices:admin/settings/update", + "indices:admin/mapping/put", + "indices:admin/mappings/get", + "indices:admin/cache/clear", + "indices:admin/aliases" + ) + .on(INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("*")) + ); + + private static final User USER_ALLOWED_TO_CREATE_INDEX = new User("user-allowed-to-create-index").roles( + new Role("create-index-role").indexPermissions("indices:admin/create").on("*") + ); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .anonymousAuth(false) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users( + ADMIN_USER, + LIMITED_READ_USER, + LIMITED_WRITE_USER, + DOUBLE_READER_USER, + REINDEXING_USER, + UPDATE_DELETE_USER, + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES, + USER_ALLOWED_TO_CREATE_INDEX + ) + .audit( + new AuditConfiguration(true).compliance(new AuditCompliance().enabled(true)) + .filters(new AuditFilters().enabledRest(true).enabledTransport(true)) + ) + .nodeSettings(Map.of(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), true)) + .build(); + + @Rule + public AuditLogsRule auditLogsRule = new AuditLogsRule(); + + @BeforeClass + public static void createTestData() { + try (Client client = cluster.getInternalNodeClient()) { + client.prepareIndex(SONG_INDEX_NAME).setId(ID_S1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); + client.prepareIndex(UPDATE_DELETE_OPERATION_INDEX_NAME) + .setId(DOCUMENT_TO_UPDATE_ID) + .setRefreshPolicy(IMMEDIATE) + .setSource("field", "value") + .get(); + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction(new AliasActions(ADD).indices(SONG_INDEX_NAME).alias(SONG_LYRICS_ALIAS)) + ) + .actionGet(); + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(SONG_INDEX_NAME).id(ID_S2).source(SONGS[1].asMap())) + .actionGet(); + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(SONG_INDEX_NAME).id(ID_S3).source(SONGS[2].asMap())) + .actionGet(); + + client.prepareIndex(PROHIBITED_SONG_INDEX_NAME).setId(ID_P4).setSource(SONGS[3].asMap()).setRefreshPolicy(IMMEDIATE).get(); + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new AliasActions(ADD).indices(PROHIBITED_SONG_INDEX_NAME).alias(PROHIBITED_SONG_ALIAS) + ) + ) + .actionGet(); + + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new AliasActions(ADD).indices(SONG_INDEX_NAME, PROHIBITED_SONG_INDEX_NAME).alias(COLLECTIVE_INDEX_ALIAS) + ) + ) + .actionGet(); + var createTemplateRequest = new org.opensearch.action.admin.indices.template.put.PutIndexTemplateRequest( + UNDELETABLE_TEMPLATE_NAME + ); + createTemplateRequest.patterns(List.of("pattern-does-not-match-to-any-index")); + createTemplateRequest.alias(new Alias(ALIAS_FROM_UNDELETABLE_TEMPLATE)); + client.admin().indices().putTemplate(createTemplateRequest).actionGet(); + + client.admin() + .cluster() + .putRepository( + new PutRepositoryRequest(UNUSED_SNAPSHOT_REPOSITORY_NAME).type("fs") + .settings(Map.of("location", cluster.getSnapshotDirPath())) + ) + .actionGet(); + } + } + + @Before + public void retrieveClusterClient() { + this.internalClient = cluster.getInternalNodeClient(); + } + + @After + public void cleanData() throws ExecutionException, InterruptedException { + Stopwatch stopwatch = Stopwatch.createStarted(); + IndicesAdminClient indices = internalClient.admin().indices(); + List indicesToBeDeleted = List.of( + WRITE_SONG_INDEX_NAME, + INDEX_NAME_SONG_TRANSCRIPTION_JAZZ, + RESTORED_SONG_INDEX_NAME, + INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("*") + ); + for (String indexToBeDeleted : indicesToBeDeleted) { + IndicesExistsRequest indicesExistsRequest = new IndicesExistsRequest(indexToBeDeleted); + var indicesExistsResponse = indices.exists(indicesExistsRequest).get(); + if (indicesExistsResponse.isExists()) { + DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(indexToBeDeleted); + indices.delete(deleteIndexRequest).actionGet(); + Awaitility.await().ignoreExceptions().until(() -> !indices.exists(indicesExistsRequest).get().isExists()); + } + } + + List aliasesToBeDeleted = List.of( + TEMPORARY_ALIAS_NAME, + ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001, + ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002, + ALIAS_CREATE_INDEX_WITH_ALIAS_POSITIVE, + ALIAS_CREATE_INDEX_WITH_ALIAS_NEGATIVE + ); + for (String aliasToBeDeleted : aliasesToBeDeleted) { + if (indices.exists(new IndicesExistsRequest(aliasToBeDeleted)).get().isExists()) { + AliasActions aliasAction = new AliasActions(AliasActions.Type.REMOVE).indices(SONG_INDEX_NAME).alias(aliasToBeDeleted); + internalClient.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(aliasAction)).get(); + } + } + + GetIndexTemplatesResponse response = indices.getTemplates(new GetIndexTemplatesRequest(MUSICAL_INDEX_TEMPLATE)).get(); + for (IndexTemplateMetadata metadata : response.getIndexTemplates()) { + indices.deleteTemplate(new DeleteIndexTemplateRequest(metadata.getName())).get(); + } + + ClusterAdminClient clusterClient = internalClient.admin().cluster(); + try { + clusterClient.deleteRepository(new DeleteRepositoryRequest(TEST_SNAPSHOT_REPOSITORY_NAME)).actionGet(); + } catch (RepositoryMissingException e) { + log.debug("Repository '{}' does not exist. This is expected in most of test cases", TEST_SNAPSHOT_REPOSITORY_NAME, e); + } + internalClient.close(); + log.debug("Cleaning data after test took {}", stopwatch.stop()); + } + + @Test + public void shouldSearchForDocuments_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S1)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/song_lyrics/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldSearchForDocuments_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(PROHIBITED_SONG_INDEX_NAME, QUERY_TITLE_POISON); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/prohibited_song_lyrics/_search")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldSearchForDocumentsViaAlias_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(SONG_LYRICS_ALIAS, QUERY_TITLE_MAGNUM_OPUS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S1)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/song_lyrics_index_alias/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldSearchForDocumentsViaAlias_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(PROHIBITED_SONG_ALIAS, QUERY_TITLE_POISON); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/prohibited_song_lyrics_index_alias/_search") + ); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldBeAbleToSearchSongViaMultiIndexAlias_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DOUBLE_READER_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(COLLECTIVE_INDEX_ALIAS, QUERY_TITLE_NEXT_SONG); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S3)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_NEXT_SONG)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/collective-index-alias/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(DOUBLE_READER_USER, "SearchRequest")); + } + + @Test + public void shouldBeAbleToSearchSongViaMultiIndexAlias_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(COLLECTIVE_INDEX_ALIAS, QUERY_TITLE_POISON); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/collective-index-alias/_search")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldBeAbleToSearchAllIndexes_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(QUERY_TITLE_MAGNUM_OPUS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S1)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(ADMIN_USER).withRestRequest(POST, "/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(ADMIN_USER, "SearchRequest")); + } + + @Test + public void shouldBeAbleToSearchAllIndexes_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(QUERY_TITLE_MAGNUM_OPUS); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_search")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldBeAbleToSearchSongIndexesWithAsterisk_prohibitedSongIndex_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DOUBLE_READER_USER)) { + SearchRequest searchRequest = queryStringQueryRequest("*" + SONG_INDEX_NAME, QUERY_TITLE_POISON); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, PROHIBITED_SONG_INDEX_NAME, ID_P4)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_POISON)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/*song_lyrics/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(DOUBLE_READER_USER, "SearchRequest")); + } + + @Test + public void shouldBeAbleToSearchSongIndexesWithAsterisk_singIndex_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DOUBLE_READER_USER)) { + SearchRequest searchRequest = queryStringQueryRequest("*" + SONG_INDEX_NAME, QUERY_TITLE_NEXT_SONG); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S3)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_NEXT_SONG)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/*song_lyrics/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(DOUBLE_READER_USER, "SearchRequest")); + } + + @Test + public void shouldBeAbleToSearchSongIndexesWithAsterisk_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = queryStringQueryRequest("*" + SONG_INDEX_NAME, QUERY_TITLE_NEXT_SONG); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/*song_lyrics/_search")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldFindSongUsingDslQuery_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = new SearchRequest(SONG_INDEX_NAME); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); + boolQueryBuilder.filter(QueryBuilders.regexpQuery(FIELD_ARTIST, "f.+")); + boolQueryBuilder.filter(new MatchQueryBuilder(FIELD_TITLE, TITLE_MAGNUM_OPUS)); + searchSourceBuilder.query(boolQueryBuilder); + searchRequest.source(searchSourceBuilder); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S1)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/song_lyrics/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldFindSongUsingDslQuery_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = new SearchRequest(PROHIBITED_SONG_INDEX_NAME); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); + boolQueryBuilder.filter(QueryBuilders.regexpQuery(FIELD_ARTIST, "n.+")); + boolQueryBuilder.filter(new MatchQueryBuilder(FIELD_TITLE, TITLE_POISON)); + searchSourceBuilder.query(boolQueryBuilder); + searchRequest.source(searchSourceBuilder); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/prohibited_song_lyrics/_search")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldPerformSearchWithAllIndexAlias_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { + SearchRequest searchRequest = queryStringQueryRequest("_all", QUERY_TITLE_MAGNUM_OPUS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S1)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(ADMIN_USER).withRestRequest(POST, "/_all/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(ADMIN_USER, "SearchRequest")); + } + + @Test + public void shouldPerformSearchWithAllIndexAlias_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = queryStringQueryRequest("_all", QUERY_TITLE_MAGNUM_OPUS); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_all/_search")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldScrollOverSearchResults_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = searchRequestWithScroll(SONG_INDEX_NAME, 2); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containNotEmptyScrollingId()); + + SearchScrollRequest scrollRequest = getSearchScrollRequest(searchResponse); + + SearchResponse scrollResponse = restHighLevelClient.scroll(scrollRequest, DEFAULT); + assertThat(scrollResponse, isSuccessfulSearchResponse()); + assertThat(scrollResponse, containNotEmptyScrollingId()); + assertThat(scrollResponse, numberOfTotalHitsIsEqualTo(3)); + assertThat(scrollResponse, numberOfHitsInPageIsEqualTo(1)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/song_lyrics/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest")); + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_search/scroll")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchScrollRequest")); + } + + @Test + public void shouldScrollOverSearchResults_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DOUBLE_READER_USER)) { + SearchRequest searchRequest = searchRequestWithScroll(SONG_INDEX_NAME, 2); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containNotEmptyScrollingId()); + + SearchScrollRequest scrollRequest = getSearchScrollRequest(searchResponse); + + assertThatThrownBy(() -> restHighLevelClient.scroll(scrollRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/song_lyrics/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(DOUBLE_READER_USER, "SearchRequest")); + auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/_search/scroll")); + auditLogsRule.assertExactlyOne(missingPrivilege(DOUBLE_READER_USER, "SearchScrollRequest")); + } + + @Test + public void shouldGetDocument_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + GetResponse response = restHighLevelClient.get(new GetRequest(SONG_INDEX_NAME, ID_S1), DEFAULT); + + assertThat(response, containDocument(SONG_INDEX_NAME, ID_S1)); + assertThat(response, documentContainField(FIELD_TITLE, TITLE_MAGNUM_OPUS)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(GET, "/song_lyrics/_doc/1")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "GetRequest")); + } + + @Test + public void shouldGetDocument_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + GetRequest getRequest = new GetRequest(PROHIBITED_SONG_INDEX_NAME, ID_P4); + assertThatThrownBy(() -> restHighLevelClient.get(getRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(GET, "/prohibited_song_lyrics/_doc/4")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "GetRequest")); + } + + @Test + public void shouldPerformMultiGetDocuments_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + MultiGetRequest request = new MultiGetRequest(); + request.add(new Item(SONG_INDEX_NAME, ID_S1)); + request.add(new Item(SONG_INDEX_NAME, ID_S2)); + + MultiGetResponse response = restHighLevelClient.mget(request, DEFAULT); + + assertThat(response, is(notNullValue())); + assertThat(response, isSuccessfulMultiGetResponse()); + assertThat(response, numberOfGetItemResponsesIsEqualTo(2)); + + MultiGetItemResponse[] responses = response.getResponses(); + assertThat( + responses[0].getResponse(), + allOf(containDocument(SONG_INDEX_NAME, ID_S1), documentContainField(FIELD_TITLE, TITLE_MAGNUM_OPUS)) + ); + assertThat( + responses[1].getResponse(), + allOf(containDocument(SONG_INDEX_NAME, ID_S2), documentContainField(FIELD_TITLE, TITLE_SONG_1_PLUS_1)) + ); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_mget")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "MultiGetRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "MultiGetShardRequest")); + } + + @Test + public void shouldPerformMultiGetDocuments_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DOUBLE_READER_USER)) { + MultiGetRequest request = new MultiGetRequest(); + request.add(new Item(SONG_INDEX_NAME, ID_S1)); + + assertThatThrownBy(() -> restHighLevelClient.mget(request, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/_mget")); + auditLogsRule.assertExactlyOne(missingPrivilege(DOUBLE_READER_USER, "MultiGetRequest")); + } + + @Test + public void shouldPerformMultiGetDocuments_partiallyPositive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + MultiGetRequest request = new MultiGetRequest(); + request.add(new Item(SONG_INDEX_NAME, ID_S1)); + request.add(new Item(PROHIBITED_SONG_INDEX_NAME, ID_P4)); + + MultiGetResponse response = restHighLevelClient.mget(request, DEFAULT); + + assertThat(request, notNullValue()); + assertThat(response, not(isSuccessfulMultiGetResponse())); + assertThat(response, numberOfGetItemResponsesIsEqualTo(2)); + + MultiGetItemResponse[] responses = response.getResponses(); + assertThat(responses, arrayContaining(hasProperty("failure", nullValue()), hasProperty("failure", notNullValue()))); + assertThat(responses[1].getFailure().getFailure(), statusException(INTERNAL_SERVER_ERROR)); + assertThat(responses[1].getFailure().getFailure(), errorMessageContain("security_exception")); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_mget")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "MultiGetRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "MultiGetShardRequest").withIndex(SONG_INDEX_NAME)); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "MultiGetShardRequest").withIndex(PROHIBITED_SONG_INDEX_NAME)); + } + + @Test + public void shouldBeAllowedToPerformMulitSearch_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + MultiSearchRequest request = new MultiSearchRequest(); + request.add(queryStringQueryRequest(SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS)); + request.add(queryStringQueryRequest(SONG_INDEX_NAME, QUERY_TITLE_NEXT_SONG)); + + MultiSearchResponse response = restHighLevelClient.msearch(request, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response, isSuccessfulMultiSearchResponse()); + assertThat(response, numberOfSearchItemResponsesIsEqualTo(2)); + + MultiSearchResponse.Item[] responses = response.getResponses(); + + assertThat(responses[0].getResponse(), searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + assertThat(responses[0].getResponse(), searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S1)); + assertThat(responses[1].getResponse(), searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_NEXT_SONG)); + assertThat(responses[1].getResponse(), searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S3)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_msearch")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "MultiSearchRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldBeAllowedToPerformMulitSearch_partiallyPositive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + MultiSearchRequest request = new MultiSearchRequest(); + request.add(queryStringQueryRequest(SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS)); + request.add(queryStringQueryRequest(PROHIBITED_SONG_INDEX_NAME, QUERY_TITLE_POISON)); + + MultiSearchResponse response = restHighLevelClient.msearch(request, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response, not(isSuccessfulMultiSearchResponse())); + assertThat(response, numberOfSearchItemResponsesIsEqualTo(2)); + + MultiSearchResponse.Item[] responses = response.getResponses(); + assertThat(responses[0].getFailure(), nullValue()); + assertThat(responses[1].getFailure(), statusException(INTERNAL_SERVER_ERROR)); + assertThat(responses[1].getFailure(), errorMessageContain("security_exception")); + assertThat(responses[1].getResponse(), nullValue()); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_msearch")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "MultiSearchRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest").withIndex(SONG_INDEX_NAME)); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest").withIndex(PROHIBITED_SONG_INDEX_NAME)); + } + + @Test + public void shouldBeAllowedToPerformMulitSearch_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DOUBLE_READER_USER)) { + MultiSearchRequest request = new MultiSearchRequest(); + request.add(queryStringQueryRequest(SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS)); + request.add(queryStringQueryRequest(SONG_INDEX_NAME, QUERY_TITLE_NEXT_SONG)); + + assertThatThrownBy(() -> restHighLevelClient.msearch(request, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/_msearch")); + auditLogsRule.assertExactlyOne(missingPrivilege(DOUBLE_READER_USER, "MultiSearchRequest")); + } + + @Test + public void shouldAggregateDataAndComputeAverage_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + final String aggregationName = "averageStars"; + SearchRequest searchRequest = averageAggregationRequest(SONG_INDEX_NAME, aggregationName, FIELD_STARS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "avg")); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/song_lyrics/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest").withIndex(SONG_INDEX_NAME)); + } + + @Test + public void shouldAggregateDataAndComputeAverage_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = averageAggregationRequest(PROHIBITED_SONG_INDEX_NAME, "averageStars", FIELD_STARS); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/prohibited_song_lyrics/_search")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest").withIndex(PROHIBITED_SONG_INDEX_NAME)); + } + + @Test + public void shouldPerformStatAggregation_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + final String aggregationName = "statsStars"; + SearchRequest searchRequest = statsAggregationRequest(SONG_INDEX_NAME, aggregationName, FIELD_STARS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "stats")); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/song_lyrics/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldPerformStatAggregation_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = statsAggregationRequest(PROHIBITED_SONG_INDEX_NAME, "statsStars", FIELD_STARS); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/prohibited_song_lyrics/_search")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldIndexDocumentInBulkRequest_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("two").source(SONGS[1].asMap())); + bulkRequest.setRefreshPolicy(IMMEDIATE); + + BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); + + assertThat(response, successBulkResponse()); + assertThat(internalClient, clusterContainsDocument(WRITE_SONG_INDEX_NAME, "one")); + assertThat(internalClient, clusterContainsDocument(WRITE_SONG_INDEX_NAME, "two")); + assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "one", FIELD_TITLE, TITLE_MAGNUM_OPUS)); + assertThat( + internalClient, + clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "two", FIELD_TITLE, TITLE_SONG_1_PLUS_1) + ); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactlyOne(auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + } + + @Test + public void shouldIndexDocumentInBulkRequest_partiallyPositive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.add(new IndexRequest(SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("two").source(SONGS[1].asMap())); + bulkRequest.setRefreshPolicy(IMMEDIATE); + + BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); + + assertThat( + response, + bulkResponseContainExceptions(0, allOf(statusException(INTERNAL_SERVER_ERROR), errorMessageContain("security_exception"))) + ); + assertThat(internalClient, clusterContainsDocument(WRITE_SONG_INDEX_NAME, "two")); + assertThat( + internalClient, + clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "two", FIELD_TITLE, TITLE_SONG_1_PLUS_1) + ); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactlyOne(auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest").withIndex(SONG_INDEX_NAME)); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + } + + @Test + public void shouldIndexDocumentInBulkRequest_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.add(new IndexRequest(SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); + bulkRequest.add(new IndexRequest(SONG_INDEX_NAME).id("two").source(SONGS[1].asMap())); + bulkRequest.setRefreshPolicy(IMMEDIATE); + + BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); + + assertThat( + response, + allOf( + failureBulkResponse(), + bulkResponseContainExceptions(statusException(INTERNAL_SERVER_ERROR)), + bulkResponseContainExceptions(errorMessageContain("security_exception")) + ) + ); + assertThat(internalClient, not(clusterContainsDocument(SONG_INDEX_NAME, "one"))); + assertThat(internalClient, not(clusterContainsDocument(SONG_INDEX_NAME, "two"))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest").withIndex(SONG_INDEX_NAME)); + } + + @Test + public void shouldUpdateDocumentsInBulk_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + final String titleOne = "shape of my mind"; + final String titleTwo = "forgiven"; + BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("two").source(SONGS[1].asMap())); + restHighLevelClient.bulk(bulkRequest, DEFAULT); + bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new UpdateRequest(WRITE_SONG_INDEX_NAME, "one").doc(Map.of(FIELD_TITLE, titleOne))); + bulkRequest.add(new UpdateRequest(WRITE_SONG_INDEX_NAME, "two").doc(Map.of(FIELD_TITLE, titleTwo))); + + BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); + + assertThat(response, successBulkResponse()); + assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "one", FIELD_TITLE, titleOne)); + assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "two", FIELD_TITLE, titleTwo)); + } + auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + auditLogsRule.assertExactlyOne(auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + } + + @Test + public void shouldUpdateDocumentsInBulk_partiallyPositive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + final String titleOne = "shape of my mind"; + BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); + restHighLevelClient.bulk(bulkRequest, DEFAULT); + bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new UpdateRequest(WRITE_SONG_INDEX_NAME, "one").doc(Map.of(FIELD_TITLE, titleOne))); + bulkRequest.add(new UpdateRequest(SONG_INDEX_NAME, ID_S2).doc(Map.of(FIELD_TITLE, "forgiven"))); + + BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); + + assertThat( + response, + bulkResponseContainExceptions(1, allOf(statusException(INTERNAL_SERVER_ERROR), errorMessageContain("security_exception"))) + ); + assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "one", FIELD_TITLE, titleOne)); + assertThat(internalClient, clusterContainsDocumentWithFieldValue(SONG_INDEX_NAME, ID_S2, FIELD_TITLE, TITLE_SONG_1_PLUS_1)); + } + auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactlyOne(auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest").withIndex(SONG_INDEX_NAME)); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + } + + @Test + public void shouldUpdateDocumentsInBulk_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new UpdateRequest(SONG_INDEX_NAME, ID_S1).doc(Map.of(FIELD_TITLE, "shape of my mind"))); + bulkRequest.add(new UpdateRequest(SONG_INDEX_NAME, ID_S2).doc(Map.of(FIELD_TITLE, "forgiven"))); + + BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); + + assertThat( + response, + allOf( + failureBulkResponse(), + bulkResponseContainExceptions(statusException(INTERNAL_SERVER_ERROR)), + bulkResponseContainExceptions(errorMessageContain("security_exception")) + ) + ); + assertThat(internalClient, clusterContainsDocumentWithFieldValue(SONG_INDEX_NAME, ID_S1, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + assertThat(internalClient, clusterContainsDocumentWithFieldValue(SONG_INDEX_NAME, ID_S2, FIELD_TITLE, TITLE_SONG_1_PLUS_1)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest")); + } + + @Test + public void shouldDeleteDocumentInBulk_positive() throws IOException { + // create index + Settings sourceIndexSettings = Settings.builder().put("index.number_of_replicas", 2).put("index.number_of_shards", 2).build(); + IndexOperationsHelper.createIndex(cluster, WRITE_SONG_INDEX_NAME, sourceIndexSettings); + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("two").source(SONGS[1].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("three").source(SONGS[2].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("four").source(SONGS[3].asMap())); + assertThat(restHighLevelClient.bulk(bulkRequest, DEFAULT), successBulkResponse()); + bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new DeleteRequest(WRITE_SONG_INDEX_NAME, "one")); + bulkRequest.add(new DeleteRequest(WRITE_SONG_INDEX_NAME, "three")); + + BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); + + assertThat(response, successBulkResponse()); + assertThat(internalClient, not(clusterContainsDocument(WRITE_SONG_INDEX_NAME, "one"))); + assertThat(internalClient, not(clusterContainsDocument(WRITE_SONG_INDEX_NAME, "three"))); + assertThat( + internalClient, + clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "two", FIELD_TITLE, TITLE_SONG_1_PLUS_1) + ); + assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "four", FIELD_TITLE, TITLE_POISON)); + } + auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactly(2, auditPredicate(null).withLayer(AuditLog.Origin.TRANSPORT)); + auditLogsRule.assertAtLeastTransportMessages(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + auditLogsRule.assertAtLeastTransportMessages(4, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + } + + @Test + public void shouldDeleteDocumentInBulk_partiallyPositive() throws IOException { + Settings indexSettings = Settings.builder().put("index.number_of_replicas", 0).put("index.number_of_shards", 1).build(); + IndexOperationsHelper.createIndex(cluster, WRITE_SONG_INDEX_NAME, indexSettings); + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("two").source(SONGS[1].asMap())); + assertThat(restHighLevelClient.bulk(bulkRequest, DEFAULT), successBulkResponse()); + bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new DeleteRequest(WRITE_SONG_INDEX_NAME, "one")); + bulkRequest.add(new DeleteRequest(SONG_INDEX_NAME, ID_S3)); + + BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); + assertThat(internalClient, not(clusterContainsDocument(WRITE_SONG_INDEX_NAME, "one"))); + + assertThat( + response, + bulkResponseContainExceptions(1, allOf(statusException(INTERNAL_SERVER_ERROR), errorMessageContain("security_exception"))) + ); + assertThat( + internalClient, + clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "two", FIELD_TITLE, TITLE_SONG_1_PLUS_1) + ); + assertThat(internalClient, clusterContainsDocumentWithFieldValue(SONG_INDEX_NAME, ID_S3, FIELD_TITLE, TITLE_NEXT_SONG)); + } + auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest")); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + } + + @Test + public void shouldDeleteDocumentInBulk_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new DeleteRequest(SONG_INDEX_NAME, ID_S1)); + bulkRequest.add(new DeleteRequest(SONG_INDEX_NAME, ID_S3)); + + BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); + + assertThat( + response, + allOf( + failureBulkResponse(), + bulkResponseContainExceptions(statusException(INTERNAL_SERVER_ERROR)), + bulkResponseContainExceptions(errorMessageContain("security_exception")) + ) + ); + assertThat(internalClient, clusterContainsDocument(SONG_INDEX_NAME, ID_S1)); + assertThat(internalClient, clusterContainsDocument(SONG_INDEX_NAME, ID_S3)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest")); + + } + + @Test + public void shouldReindexDocuments_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(REINDEXING_USER)) { + ReindexRequest reindexRequest = new ReindexRequest().setSourceIndices(SONG_INDEX_NAME).setDestIndex(WRITE_SONG_INDEX_NAME); + + BulkByScrollResponse response = restHighLevelClient.reindex(reindexRequest, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response.getBulkFailures(), empty()); + assertThat(response.getSearchFailures(), empty()); + assertThat(internalClient, clusterContainsDocument(WRITE_SONG_INDEX_NAME, ID_S1)); + assertThat(internalClient, clusterContainsDocument(WRITE_SONG_INDEX_NAME, ID_S2)); + assertThat(internalClient, clusterContainsDocument(WRITE_SONG_INDEX_NAME, ID_S3)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(REINDEXING_USER).withRestRequest(POST, "/_reindex")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "ReindexRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "SearchRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "BulkRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "CreateIndexRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "SearchScrollRequest")); + auditLogsRule.assertExactlyOne(auditPredicate(INDEX_EVENT).withEffectiveUser(REINDEXING_USER)); + auditLogsRule.assertExactlyOne(missingPrivilege(REINDEXING_USER, "ClearScrollRequest")); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(REINDEXING_USER, "PutMappingRequest")); + } + + @Test + public void shouldReindexDocuments_negativeSource() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(REINDEXING_USER)) { + ReindexRequest reindexRequest = new ReindexRequest().setSourceIndices(PROHIBITED_SONG_INDEX_NAME) + .setDestIndex(WRITE_SONG_INDEX_NAME); + + assertThatThrownBy(() -> restHighLevelClient.reindex(reindexRequest, DEFAULT), statusException(FORBIDDEN)); + assertThat(internalClient, not(clusterContainsDocument(WRITE_SONG_INDEX_NAME, ID_P4))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(REINDEXING_USER).withRestRequest(POST, "/_reindex")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "ReindexRequest")); + auditLogsRule.assertExactlyOne(missingPrivilege(REINDEXING_USER, "SearchRequest")); + } + + @Test + public void shouldReindexDocuments_negativeDestination() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(REINDEXING_USER)) { + ReindexRequest reindexRequest = new ReindexRequest().setSourceIndices(SONG_INDEX_NAME).setDestIndex(PROHIBITED_SONG_INDEX_NAME); + + assertThatThrownBy(() -> restHighLevelClient.reindex(reindexRequest, DEFAULT), statusException(FORBIDDEN)); + assertThat(internalClient, not(clusterContainsDocument(PROHIBITED_SONG_INDEX_NAME, ID_S1))); + assertThat(internalClient, not(clusterContainsDocument(PROHIBITED_SONG_INDEX_NAME, ID_S2))); + assertThat(internalClient, not(clusterContainsDocument(PROHIBITED_SONG_INDEX_NAME, ID_S3))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(REINDEXING_USER).withRestRequest(POST, "/_reindex")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "ReindexRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "SearchRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "BulkRequest")); + auditLogsRule.assertExactlyOne(missingPrivilege(REINDEXING_USER, "BulkShardRequest")); + auditLogsRule.assertExactlyOne(missingPrivilege(REINDEXING_USER, "ClearScrollRequest")); + } + + @Test + public void shouldReindexDocuments_negativeSourceAndDestination() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(REINDEXING_USER)) { + ReindexRequest reindexRequest = new ReindexRequest().setSourceIndices(PROHIBITED_SONG_INDEX_NAME).setDestIndex(SONG_INDEX_NAME); + + assertThatThrownBy(() -> restHighLevelClient.reindex(reindexRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(REINDEXING_USER).withRestRequest(POST, "/_reindex")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "ReindexRequest")); + auditLogsRule.assertExactlyOne(missingPrivilege(REINDEXING_USER, "SearchRequest")); + } + + @Test + public void shouldUpdateDocument_positive() throws IOException { + String newField = "newField"; + String newValue = "newValue"; + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(UPDATE_DELETE_USER)) { + UpdateRequest updateRequest = new UpdateRequest(UPDATE_DELETE_OPERATION_INDEX_NAME, DOCUMENT_TO_UPDATE_ID).doc( + newField, + newValue + ).setRefreshPolicy(IMMEDIATE); + + UpdateResponse response = restHighLevelClient.update(updateRequest, DEFAULT); + + assertThat(response, isSuccessfulUpdateResponse()); + assertThat( + internalClient, + clusterContainsDocumentWithFieldValue(UPDATE_DELETE_OPERATION_INDEX_NAME, DOCUMENT_TO_UPDATE_ID, newField, newValue) + ); + } + } + + @Test + public void shouldUpdateDocument_negative() throws IOException { + String newField = "newField"; + String newValue = "newValue"; + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(UPDATE_DELETE_USER)) { + UpdateRequest updateRequest = new UpdateRequest(PROHIBITED_SONG_INDEX_NAME, DOCUMENT_TO_UPDATE_ID).doc(newField, newValue) + .setRefreshPolicy(IMMEDIATE); + + assertThatThrownBy(() -> restHighLevelClient.update(updateRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldDeleteDocument_positive() throws IOException { + String docId = "shouldDeleteDocument_positive"; + try (Client client = cluster.getInternalNodeClient()) { + client.index( + new IndexRequest(UPDATE_DELETE_OPERATION_INDEX_NAME).id(docId).source("field", "value").setRefreshPolicy(IMMEDIATE) + ).actionGet(); + assertThat(internalClient, clusterContainsDocument(UPDATE_DELETE_OPERATION_INDEX_NAME, docId)); + } + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(UPDATE_DELETE_USER)) { + DeleteRequest deleteRequest = new DeleteRequest(UPDATE_DELETE_OPERATION_INDEX_NAME, docId).setRefreshPolicy(IMMEDIATE); + + DeleteResponse response = restHighLevelClient.delete(deleteRequest, DEFAULT); + + assertThat(response, isSuccessfulDeleteResponse()); + assertThat(internalClient, not(clusterContainsDocument(UPDATE_DELETE_OPERATION_INDEX_NAME, docId))); + } + } + + @Test + public void shouldDeleteDocument_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(UPDATE_DELETE_USER)) { + DeleteRequest deleteRequest = new DeleteRequest(PROHIBITED_SONG_INDEX_NAME, ID_S1).setRefreshPolicy(IMMEDIATE); + + assertThatThrownBy(() -> restHighLevelClient.delete(deleteRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldCreateAlias_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + AliasActions aliasAction = new AliasActions(ADD).indices(SONG_INDEX_NAME).alias(TEMPORARY_ALIAS_NAME); + IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest().addAliasAction(aliasAction); + + var response = restHighLevelClient.indices().updateAliases(indicesAliasesRequest, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response.isAcknowledged(), equalTo(true)); + assertThat(internalClient, clusterContainsDocument(TEMPORARY_ALIAS_NAME, ID_S1)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_aliases")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "IndicesAliasesRequest")); + auditLogsRule.assertExactlyOne(auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_READ_USER)); + } + + @Test + public void shouldCreateAlias_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + AliasActions aliasAction = new AliasActions(ADD).indices(PROHIBITED_SONG_INDEX_NAME).alias(TEMPORARY_ALIAS_NAME); + IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest().addAliasAction(aliasAction); + + assertThatThrownBy( + () -> restHighLevelClient.indices().updateAliases(indicesAliasesRequest, DEFAULT), + statusException(FORBIDDEN) + ); + + assertThat(internalClient, not(clusterContainsDocument(TEMPORARY_ALIAS_NAME, ID_P4))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_aliases")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "IndicesAliasesRequest")); + } + + @Test + public void shouldDeleteAlias_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + AliasActions aliasAction = new AliasActions(ADD).indices(SONG_INDEX_NAME).alias(TEMPORARY_ALIAS_NAME); + IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest().addAliasAction(aliasAction); + restHighLevelClient.indices().updateAliases(indicesAliasesRequest, DEFAULT); + aliasAction = new AliasActions(REMOVE).indices(SONG_INDEX_NAME).alias(TEMPORARY_ALIAS_NAME); + indicesAliasesRequest = new IndicesAliasesRequest().addAliasAction(aliasAction); + + var response = restHighLevelClient.indices().updateAliases(indicesAliasesRequest, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response.isAcknowledged(), equalTo(true)); + assertThat(internalClient, not(clusterContainsDocument(TEMPORARY_ALIAS_NAME, ID_S1))); + } + auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_aliases")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_READ_USER, "IndicesAliasesRequest")); + auditLogsRule.assertExactly(2, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_READ_USER)); + } + + @Test + public void shouldDeleteAlias_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + AliasActions aliasAction = new AliasActions(REMOVE).indices(PROHIBITED_SONG_INDEX_NAME).alias(PROHIBITED_SONG_ALIAS); + IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest().addAliasAction(aliasAction); + + assertThatThrownBy( + () -> restHighLevelClient.indices().updateAliases(indicesAliasesRequest, DEFAULT), + statusException(FORBIDDEN) + ); + + assertThat(internalClient, clusterContainsDocument(PROHIBITED_SONG_INDEX_NAME, ID_P4)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_aliases")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "IndicesAliasesRequest")); + } + + @Test + public void shouldCreateIndexTemplate_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + PutIndexTemplateRequest request = new PutIndexTemplateRequest(MUSICAL_INDEX_TEMPLATE).patterns(List.of(TEMPLATE_INDEX_PREFIX)) + .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001)) + .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002)); + + var response = restHighLevelClient.indices().putTemplate(request, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response.isAcknowledged(), equalTo(true)); + assertThat(internalClient, clusterContainTemplate(MUSICAL_INDEX_TEMPLATE)); + String documentId = "0001"; + IndexRequest indexRequest = new IndexRequest(INDEX_NAME_SONG_TRANSCRIPTION_JAZZ).id(documentId) + .source(SONGS[0].asMap()) + .setRefreshPolicy(IMMEDIATE); + restHighLevelClient.index(indexRequest, DEFAULT); + assertThat(internalClient, clusterContainsDocument(INDEX_NAME_SONG_TRANSCRIPTION_JAZZ, documentId)); + assertThat(internalClient, clusterContainsDocument(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001, documentId)); + assertThat(internalClient, clusterContainsDocument(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002, documentId)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_template/musical-index-template")); + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/song-transcription-jazz/_doc/0001")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutIndexTemplateRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "IndexRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactly(2, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + } + + @Test + public void shouldCreateIndexTemplate_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + PutIndexTemplateRequest request = new PutIndexTemplateRequest(MUSICAL_INDEX_TEMPLATE).patterns(List.of(TEMPLATE_INDEX_PREFIX)) + .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001)) + .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002)); + + assertThatThrownBy(() -> restHighLevelClient.indices().putTemplate(request, DEFAULT), statusException(FORBIDDEN)); + assertThat(internalClient, not(clusterContainTemplate(MUSICAL_INDEX_TEMPLATE))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(PUT, "/_template/musical-index-template")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "PutIndexTemplateRequest")); + } + + @Test + public void shouldDeleteTemplate_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + PutIndexTemplateRequest request = new PutIndexTemplateRequest(MUSICAL_INDEX_TEMPLATE).patterns(List.of(TEMPLATE_INDEX_PREFIX)); + restHighLevelClient.indices().putTemplate(request, DEFAULT); + assertThat(internalClient, clusterContainTemplate(MUSICAL_INDEX_TEMPLATE)); + DeleteIndexTemplateRequest deleteRequest = new DeleteIndexTemplateRequest(MUSICAL_INDEX_TEMPLATE); + + var response = restHighLevelClient.indices().deleteTemplate(deleteRequest, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response.isAcknowledged(), equalTo(true)); + assertThat(internalClient, not(clusterContainTemplate(MUSICAL_INDEX_TEMPLATE))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_template/musical-index-template")); + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(DELETE, "/_template/musical-index-template")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutIndexTemplateRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "DeleteIndexTemplateRequest")); + auditLogsRule.assertExactly(2, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + } + + @Test + public void shouldDeleteTemplate_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + DeleteIndexTemplateRequest deleteRequest = new DeleteIndexTemplateRequest(UNDELETABLE_TEMPLATE_NAME); + + assertThatThrownBy(() -> restHighLevelClient.indices().deleteTemplate(deleteRequest, DEFAULT), statusException(FORBIDDEN)); + + assertThat(internalClient, clusterContainTemplate(UNDELETABLE_TEMPLATE_NAME)); + } + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_READ_USER).withRestRequest(DELETE, "/_template/undeletable-template-name") + ); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "DeleteIndexTemplateRequest")); + } + + @Test + public void shouldUpdateTemplate_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + PutIndexTemplateRequest request = new PutIndexTemplateRequest(MUSICAL_INDEX_TEMPLATE).patterns(List.of(TEMPLATE_INDEX_PREFIX)) + .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001)) + .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002)); + restHighLevelClient.indices().putTemplate(request, DEFAULT); + assertThat(internalClient, clusterContainTemplate(MUSICAL_INDEX_TEMPLATE)); + request = new PutIndexTemplateRequest(MUSICAL_INDEX_TEMPLATE).patterns(List.of(TEMPLATE_INDEX_PREFIX)) + .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0003)); + + var response = restHighLevelClient.indices().putTemplate(request, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response.isAcknowledged(), equalTo(true)); + String documentId = "000one"; + IndexRequest indexRequest = new IndexRequest(INDEX_NAME_SONG_TRANSCRIPTION_JAZZ).id(documentId) + .source(SONGS[0].asMap()) + .setRefreshPolicy(IMMEDIATE); + restHighLevelClient.index(indexRequest, DEFAULT); + assertThat(internalClient, clusterContainTemplate(MUSICAL_INDEX_TEMPLATE)); + assertThat(internalClient, clusterContainsDocument(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0003, documentId)); + assertThat(internalClient, not(clusterContainsDocument(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001, documentId))); + assertThat(internalClient, not(clusterContainsDocument(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002, documentId))); + } + auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_template/musical-index-template")); + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/song-transcription-jazz/_doc/000one")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutIndexTemplateRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "IndexRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactly(3, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + } + + @Test + public void shouldUpdateTemplate_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + PutIndexTemplateRequest request = new PutIndexTemplateRequest(UNDELETABLE_TEMPLATE_NAME).patterns( + List.of(TEMPLATE_INDEX_PREFIX) + ).alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0003)); + + assertThatThrownBy(() -> restHighLevelClient.indices().putTemplate(request, DEFAULT), statusException(FORBIDDEN)); + assertThat(internalClient, clusterContainTemplateWithAlias(UNDELETABLE_TEMPLATE_NAME, ALIAS_FROM_UNDELETABLE_TEMPLATE)); + assertThat( + internalClient, + not(clusterContainTemplateWithAlias(UNDELETABLE_TEMPLATE_NAME, ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0003)) + ); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(PUT, "/_template/undeletable-template-name")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "PutIndexTemplateRequest")); + } + + @Test + public void shouldGetFieldCapabilitiesForAllIndexes_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { + FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().fields(FIELD_TITLE); + + FieldCapabilitiesResponse response = restHighLevelClient.fieldCaps(request, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response, containsExactlyIndices(SONG_INDEX_NAME, PROHIBITED_SONG_INDEX_NAME, UPDATE_DELETE_OPERATION_INDEX_NAME)); + assertThat(response, numberOfFieldsIsEqualTo(1)); + assertThat(response, containsFieldWithNameAndType(FIELD_TITLE, "text")); + } + auditLogsRule.assertExactlyOne(userAuthenticated(ADMIN_USER).withRestRequest(GET, "/_field_caps")); + auditLogsRule.assertExactlyOne(grantedPrivilege(ADMIN_USER, "FieldCapabilitiesRequest")); + auditLogsRule.assertExactly(3, grantedPrivilege(ADMIN_USER, "FieldCapabilitiesIndexRequest")); + } + + @Test + public void shouldGetFieldCapabilitiesForAllIndexes_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().fields(FIELD_TITLE); + + assertThatThrownBy(() -> restHighLevelClient.fieldCaps(request, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(GET, "/_field_caps")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "FieldCapabilitiesRequest")); + } + + @Test + public void shouldGetFieldCapabilitiesForParticularIndex_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().indices(SONG_INDEX_NAME).fields(FIELD_TITLE); + + FieldCapabilitiesResponse response = restHighLevelClient.fieldCaps(request, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response, containsExactlyIndices(SONG_INDEX_NAME)); + assertThat(response, numberOfFieldsIsEqualTo(1)); + assertThat(response, containsFieldWithNameAndType(FIELD_TITLE, "text")); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(GET, "/song_lyrics/_field_caps")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "FieldCapabilitiesRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "FieldCapabilitiesIndexRequest")); + } + + @Test + public void shouldGetFieldCapabilitiesForParticularIndex_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().indices(PROHIBITED_SONG_INDEX_NAME).fields(FIELD_TITLE); + + assertThatThrownBy(() -> restHighLevelClient.fieldCaps(request, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(GET, "/prohibited_song_lyrics/_field_caps")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "FieldCapabilitiesRequest")); + } + + @Test + public void shouldCreateSnapshotRepository_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + String snapshotDirPath = cluster.getSnapshotDirPath(); + + var response = steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotDirPath, "fs"); + + assertThat(response, notNullValue()); + assertThat(response.isAcknowledged(), equalTo(true)); + assertThat(internalClient, clusterContainsSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + } + + @Test + public void shouldCreateSnapshotRepository_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + String snapshotDirPath = cluster.getSnapshotDirPath(); + + assertThatThrownBy( + () -> steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotDirPath, "fs"), + statusException(FORBIDDEN) + ); + assertThat(internalClient, not(clusterContainsSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "PutRepositoryRequest")); + } + + @Test + public void shouldDeleteSnapshotRepository_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); + assertThat(internalClient, clusterContainsSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME)); + + var response = steps.deleteSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME); + + assertThat(response, notNullValue()); + assertThat(response.isAcknowledged(), equalTo(true)); + assertThat(internalClient, not(clusterContainsSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(DELETE, "/_snapshot/test-snapshot-repository") + ); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "DeleteRepositoryRequest")); + } + + @Test + public void shouldDeleteSnapshotRepository_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + + assertThatThrownBy(() -> steps.deleteSnapshotRepository(UNUSED_SNAPSHOT_REPOSITORY_NAME), statusException(FORBIDDEN)); + assertThat(internalClient, clusterContainsSnapshotRepository(UNUSED_SNAPSHOT_REPOSITORY_NAME)); + } + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_READ_USER).withRestRequest(DELETE, "/_snapshot/unused-snapshot-repository") + ); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "DeleteRepositoryRequest")); + } + + @Test + public void shouldCreateSnapshot_positive() throws IOException { + final String snapshotName = "snapshot-positive-test"; + long snapshotGetCount; + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); + + CreateSnapshotResponse response = steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, SONG_INDEX_NAME); + + assertThat(response, notNullValue()); + assertThat(response.status(), equalTo(RestStatus.ACCEPTED)); + snapshotGetCount = steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + assertThat(internalClient, clusterContainSuccessSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository/snapshot-positive-test") + ); + auditLogsRule.assertExactly( + snapshotGetCount, + userAuthenticated(LIMITED_WRITE_USER).withEffectiveUser(LIMITED_WRITE_USER) + .withRestRequest(GET, "/_snapshot/test-snapshot-repository/snapshot-positive-test") + ); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); + auditLogsRule.assertAtLeast(snapshotGetCount, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); + } + + @Test + public void shouldCreateSnapshot_negative() throws IOException { + final String snapshotName = "snapshot-negative-test"; + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + + assertThatThrownBy( + () -> steps.createSnapshot(UNUSED_SNAPSHOT_REPOSITORY_NAME, snapshotName, SONG_INDEX_NAME), + statusException(FORBIDDEN) + ); + + assertThat(internalClient, snapshotInClusterDoesNotExists(UNUSED_SNAPSHOT_REPOSITORY_NAME, snapshotName)); + } + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_READ_USER).withRestRequest(PUT, "/_snapshot/unused-snapshot-repository/snapshot-negative-test") + ); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "CreateSnapshotRequest")); + } + + @Test + public void shouldDeleteSnapshot_positive() throws IOException { + String snapshotName = "delete-snapshot-positive"; + long snapshotGetCount; + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + restHighLevelClient.snapshot(); + steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); + steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, SONG_INDEX_NAME); + snapshotGetCount = steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + + var response = steps.deleteSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + + assertThat(response.isAcknowledged(), equalTo(true)); + assertThat(internalClient, snapshotInClusterDoesNotExists(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository/delete-snapshot-positive") + ); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(DELETE, "/_snapshot/test-snapshot-repository/delete-snapshot-positive") + ); + auditLogsRule.assertExactly( + snapshotGetCount, + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(GET, "/_snapshot/test-snapshot-repository/delete-snapshot-positive") + ); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "DeleteSnapshotRequest")); + auditLogsRule.assertExactly(snapshotGetCount, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); + } + + @Test + public void shouldDeleteSnapshot_negative() throws IOException { + String snapshotName = "delete-snapshot-negative"; + long snapshotGetCount; + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); + steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, SONG_INDEX_NAME); + snapshotGetCount = steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + } + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + assertThatThrownBy(() -> steps.deleteSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName), statusException(FORBIDDEN)); + + assertThat(internalClient, clusterContainSuccessSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository/delete-snapshot-negative") + ); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_READ_USER).withRestRequest(DELETE, "/_snapshot/test-snapshot-repository/delete-snapshot-negative") + ); + auditLogsRule.assertExactly( + snapshotGetCount, + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(GET, "/_snapshot/test-snapshot-repository/delete-snapshot-negative") + ); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "DeleteSnapshotRequest")); + auditLogsRule.assertExactly(snapshotGetCount, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); + } + + @Test + public void shouldRestoreSnapshot_positive() throws IOException { + final String snapshotName = "restore-snapshot-positive"; + long snapshotGetCount; + AtomicInteger restoredCount = new AtomicInteger(); + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + // 1. create some documents + Settings indexSettings = Settings.builder().put("index.number_of_replicas", 0).put("index.number_of_shards", 1).build(); + IndexOperationsHelper.createIndex(cluster, WRITE_SONG_INDEX_NAME, indexSettings); + + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Eins").source(SONGS[0].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Zwei").source(SONGS[1].asMap())); + bulkRequest.setRefreshPolicy(IMMEDIATE); + restHighLevelClient.bulk(bulkRequest, DEFAULT); + + // 2. create snapshot repository + steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); + + // 3. create snapshot + steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, WRITE_SONG_INDEX_NAME); + + // 4. wait till snapshot is ready + snapshotGetCount = steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + + // 5. introduce some changes + bulkRequest = new BulkRequest(); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Drei").source(SONGS[2].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Vier").source(SONGS[3].asMap())); + bulkRequest.add(new DeleteRequest(WRITE_SONG_INDEX_NAME, "Eins")); + bulkRequest.setRefreshPolicy(IMMEDIATE); + restHighLevelClient.bulk(bulkRequest, DEFAULT); + + // 6. restore the snapshot + var response = steps.restoreSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, "(.+)", "restored_$1"); + + assertThat(response, notNullValue()); + assertThat(response.status(), equalTo(ACCEPTED)); + + // 7. wait until snapshot is restored + CountRequest countRequest = new CountRequest(RESTORED_SONG_INDEX_NAME); + Awaitility.await() + .ignoreExceptions() + .pollInterval(100, TimeUnit.MILLISECONDS) + .alias("Index contains proper number of documents restored from snapshot.") + .until(() -> { + restoredCount.incrementAndGet(); + return restHighLevelClient.count(countRequest, DEFAULT).getCount() == 2; + }); + + // 8. verify that document are present in restored index + assertThat( + internalClient, + clusterContainsDocumentWithFieldValue(RESTORED_SONG_INDEX_NAME, "Eins", FIELD_TITLE, TITLE_MAGNUM_OPUS) + ); + assertThat( + internalClient, + clusterContainsDocumentWithFieldValue(RESTORED_SONG_INDEX_NAME, "Zwei", FIELD_TITLE, TITLE_SONG_1_PLUS_1) + ); + assertThat(internalClient, not(clusterContainsDocument(RESTORED_SONG_INDEX_NAME, "Drei"))); + assertThat(internalClient, not(clusterContainsDocument(RESTORED_SONG_INDEX_NAME, "Vier"))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository/restore-snapshot-positive") + ); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest( + POST, + "/_snapshot/test-snapshot-repository/restore-snapshot-positive/_restore" + ) + ); + auditLogsRule.assertExactly( + restoredCount.get(), + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/restored_write_song_index/_count") + ); + auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactly( + snapshotGetCount, + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(GET, "/_snapshot/test-snapshot-repository/restore-snapshot-positive") + ); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "RestoreSnapshotRequest")); + auditLogsRule.assertExactly(restoredCount.get(), grantedPrivilege(LIMITED_WRITE_USER, "SearchRequest")); + auditLogsRule.assertExactly(snapshotGetCount, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + } + + @Test + public void shouldRestoreSnapshot_failureForbiddenIndex() throws IOException { + final String snapshotName = "restore-snapshot-negative-forbidden-index"; + String restoreToIndex = "forbidden_index"; + long snapshotGetCount; + Settings indexSettings = Settings.builder().put("index.number_of_shards", 1).put("index.number_of_replicas", 0).build(); + IndexOperationsHelper.createIndex(cluster, WRITE_SONG_INDEX_NAME, indexSettings); + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + // 1. create some documents + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Eins").source(SONGS[0].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Zwei").source(SONGS[1].asMap())); + bulkRequest.setRefreshPolicy(IMMEDIATE); + restHighLevelClient.bulk(bulkRequest, DEFAULT); + + // 2. create snapshot repository + steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); + + // 3. create snapshot + steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, WRITE_SONG_INDEX_NAME); + + // 4. wait till snapshot is ready + snapshotGetCount = steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + + // 5. restore the snapshot + assertThatThrownBy( + () -> steps.restoreSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, "(.+)", restoreToIndex), + statusException(FORBIDDEN) + ); + + // 6. verify that document are not present in restored index + assertThat(internalClient, not(clusterContainsDocument(RESTORED_SONG_INDEX_NAME, "Eins"))); + assertThat(internalClient, not(clusterContainsDocument(RESTORED_SONG_INDEX_NAME, "Zwei"))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest( + PUT, + "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-index" + ) + ); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest( + POST, + "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-index/_restore" + ) + ); + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactly( + snapshotGetCount, + userAuthenticated(LIMITED_WRITE_USER).withRestRequest( + GET, + "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-index" + ) + ); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactly(snapshotGetCount, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); + auditLogsRule.assertAtLeastTransportMessages(1, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertExactlyScanAll(1, missingPrivilege(LIMITED_WRITE_USER, "RestoreSnapshotRequest")); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + } + + @Test + public void shouldRestoreSnapshot_failureOperationForbidden() throws IOException { + String snapshotName = "restore-snapshot-negative-forbidden-operation"; + long snapshotGetCount; + Settings indexSettings = Settings.builder().put("index.number_of_shards", 1).put("index.number_of_replicas", 0).build(); + IndexOperationsHelper.createIndex(cluster, WRITE_SONG_INDEX_NAME, indexSettings); + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + // 1. create some documents + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Eins").source(SONGS[0].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Zwei").source(SONGS[1].asMap())); + bulkRequest.setRefreshPolicy(IMMEDIATE); + restHighLevelClient.bulk(bulkRequest, DEFAULT); + + // 2. create snapshot repository + steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); + + // 3. create snapshot + steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, WRITE_SONG_INDEX_NAME); + + // 4. wait till snapshot is ready + snapshotGetCount = steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + } + // 5. restore the snapshot + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + assertThatThrownBy( + () -> steps.restoreSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, "(.+)", "restored_$1"), + statusException(FORBIDDEN) + ); + + // 6. verify that documents does not exist + assertThat(internalClient, not(clusterContainsDocument(RESTORED_SONG_INDEX_NAME, "Eins"))); + assertThat(internalClient, not(clusterContainsDocument(RESTORED_SONG_INDEX_NAME, "Zwei"))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest( + PUT, + "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-operation" + ) + ); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_READ_USER).withRestRequest( + POST, + "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-operation/_restore" + ) + ); + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactly( + snapshotGetCount, + userAuthenticated(LIMITED_WRITE_USER).withRestRequest( + GET, + "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-operation" + ) + ); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactlyScanAll(1, missingPrivilege(LIMITED_READ_USER, "RestoreSnapshotRequest")); + auditLogsRule.assertExactly(snapshotGetCount, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + } + + @Test + // required permissions: "indices:admin/create" + public void createIndex_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("create_index_positive"); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName); + CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(createIndexRequest, DEFAULT); + + assertThat(createIndexResponse, isSuccessfulCreateIndexResponse(indexName)); + assertThat(cluster, indexExists(indexName)); + } + } + + @Test + public void createIndex_negative() throws IOException { + String indexName = "create_index_negative"; + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName); + + assertThatThrownBy(() -> restHighLevelClient.indices().create(createIndexRequest, DEFAULT), statusException(FORBIDDEN)); + assertThat(cluster, not(indexExists(indexName))); + } + } + + @Test + // required permissions: "indices:admin/get" + public void checkIfIndexExists_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("index_exists_positive"); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + boolean exists = restHighLevelClient.indices().exists(new GetIndexRequest(indexName), DEFAULT); + + assertThat(exists, is(false)); + } + } + + @Test + public void checkIfIndexExists_negative() throws IOException { + String indexThatUserHasNoAccessTo = "index_exists_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + assertThatThrownBy( + () -> restHighLevelClient.indices().exists(new GetIndexRequest(indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .exists(new GetIndexRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy(() -> restHighLevelClient.indices().exists(new GetIndexRequest("*"), DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + // required permissions: "indices:admin/delete" + public void deleteIndex_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("delete_index_positive"); + IndexOperationsHelper.createIndex(cluster, indexName); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(indexName); + var response = restHighLevelClient.indices().delete(deleteIndexRequest, DEFAULT); + + assertThat(response.isAcknowledged(), is(true)); + assertThat(cluster, not(indexExists(indexName))); + } + } + + @Test + public void deleteIndex_negative() throws IOException { + String indexThatUserHasNoAccessTo = "delete_index_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + + assertThatThrownBy( + () -> restHighLevelClient.indices().delete(new DeleteIndexRequest(indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .delete(new DeleteIndexRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices().delete(new DeleteIndexRequest("*"), DEFAULT), + statusException(FORBIDDEN) + ); + } + } + + @Test + // required permissions: indices:admin/aliases, indices:admin/delete + public void shouldDeleteIndexByAliasRequest_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("delete_index_by_alias_request_positive"); + IndexOperationsHelper.createIndex(cluster, indexName); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + IndicesAliasesRequest request = new IndicesAliasesRequest().addAliasAction(new AliasActions(REMOVE_INDEX).indices(indexName)); + + var response = restHighLevelClient.indices().updateAliases(request, DEFAULT); + + assertThat(response.isAcknowledged(), is(true)); + assertThat(cluster, not(indexExists(indexName))); + } + auditLogsRule.assertExactlyOne( + userAuthenticated(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES).withRestRequest(POST, "/_aliases") + ); + auditLogsRule.assertExactly( + 1, + grantedPrivilege(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES, "IndicesAliasesRequest") + ); + auditLogsRule.assertExactly( + 1, + auditPredicate(INDEX_EVENT).withEffectiveUser(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES) + ); + } + + @Test + public void shouldDeleteIndexByAliasRequest_negative() throws IOException { + String indexName = "delete_index_by_alias_request_negative"; + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + IndicesAliasesRequest request = new IndicesAliasesRequest().addAliasAction(new AliasActions(REMOVE_INDEX).indices(indexName)); + + assertThatThrownBy(() -> restHighLevelClient.indices().updateAliases(request, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + // required permissions: "indices:admin/get" + public void getIndex_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("get_index_positive"); + IndexOperationsHelper.createIndex(cluster, indexName); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + GetIndexRequest getIndexRequest = new GetIndexRequest(indexName); + GetIndexResponse response = restHighLevelClient.indices().get(getIndexRequest, DEFAULT); + + assertThat(response, getIndexResponseContainsIndices(indexName)); + } + } + + @Test + public void getIndex_negative() throws IOException { + String indexThatUserHasNoAccessTo = "get_index_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + + assertThatThrownBy( + () -> restHighLevelClient.indices().get(new GetIndexRequest(indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices().get(new GetIndexRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy(() -> restHighLevelClient.indices().get(new GetIndexRequest("*"), DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + // required permissions: "indices:admin/close", "indices:admin/close*" + public void closeIndex_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("close_index_positive"); + IndexOperationsHelper.createIndex(cluster, indexName); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + CloseIndexRequest closeIndexRequest = new CloseIndexRequest(indexName); + CloseIndexResponse response = restHighLevelClient.indices().close(closeIndexRequest, DEFAULT); + + assertThat(response, isSuccessfulCloseIndexResponse()); + assertThat(cluster, indexStateIsEqualTo(indexName, IndexMetadata.State.CLOSE)); + } + } + + @Test + public void closeIndex_negative() throws IOException { + String indexThatUserHasNoAccessTo = "close_index_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + + assertThatThrownBy( + () -> restHighLevelClient.indices().close(new CloseIndexRequest(indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .close(new CloseIndexRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy(() -> restHighLevelClient.indices().close(new CloseIndexRequest("*"), DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + // required permissions: "indices:admin/open" + public void openIndex_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("open_index_positive"); + IndexOperationsHelper.createIndex(cluster, indexName); + IndexOperationsHelper.closeIndex(cluster, indexName); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + OpenIndexRequest closeIndexRequest = new OpenIndexRequest(indexName); + OpenIndexResponse response = restHighLevelClient.indices().open(closeIndexRequest, DEFAULT); + + assertThat(response, isSuccessfulOpenIndexResponse()); + assertThat(cluster, indexStateIsEqualTo(indexName, IndexMetadata.State.OPEN)); + } + } + + @Test + public void openIndex_negative() throws IOException { + String indexThatUserHasNoAccessTo = "open_index_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + + assertThatThrownBy( + () -> restHighLevelClient.indices().open(new OpenIndexRequest(indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .open(new OpenIndexRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy(() -> restHighLevelClient.indices().open(new OpenIndexRequest("*"), DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + // required permissions: "indices:admin/resize", "indices:monitor/stats + public void shrinkIndex_positive() throws IOException { + String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("shrink_index_positive_source"); + String targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("shrink_index_positive_target"); + Settings sourceIndexSettings = Settings.builder() + .put("index.number_of_replicas", 1) + .put("index.blocks.write", true) + .put("index.number_of_shards", 4) + .build(); + IndexOperationsHelper.createIndex(cluster, sourceIndexName, sourceIndexSettings); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ClusterHealthResponse healthResponse = restHighLevelClient.cluster() + .health( + new ClusterHealthRequest(sourceIndexName).waitForNoRelocatingShards(true) + .waitForActiveShards(4) + .waitForNoInitializingShards(true) + .waitForGreenStatus(), + DEFAULT + ); + + assertThat(healthResponse.getStatus(), is(ClusterHealthStatus.GREEN)); + + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + ResizeResponse response = restHighLevelClient.indices().shrink(resizeRequest, DEFAULT); + + assertThat(response, isSuccessfulResizeResponse(targetIndexName)); + assertThat(cluster, indexExists(targetIndexName)); + } + } + + @Test + public void shrinkIndex_negative() throws IOException { + // user cannot access target index + String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("shrink_index_negative_source"); + String targetIndexName = "shrink_index_negative_target"; + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + + assertThatThrownBy(() -> restHighLevelClient.indices().shrink(resizeRequest, DEFAULT), statusException(FORBIDDEN)); + assertThat(cluster, not(indexExists(targetIndexName))); + } + + // user cannot access source index + sourceIndexName = "shrink_index_negative_source"; + targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("shrink_index_negative_target"); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + + assertThatThrownBy(() -> restHighLevelClient.indices().shrink(resizeRequest, DEFAULT), statusException(FORBIDDEN)); + assertThat(cluster, not(indexExists(targetIndexName))); + } + } + + @Test + // required permissions: "indices:admin/resize", "indices:monitor/stats + public void cloneIndex_positive() throws IOException { + String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("clone_index_positive_source"); + Settings sourceIndexSettings = Settings.builder().put("index.blocks.write", true).build(); + String targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("clone_index_positive_target"); + IndexOperationsHelper.createIndex(cluster, sourceIndexName, sourceIndexSettings); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + ResizeResponse response = restHighLevelClient.indices().clone(resizeRequest, DEFAULT); + + assertThat(response, isSuccessfulResizeResponse(targetIndexName)); + assertThat(cluster, indexExists(targetIndexName)); + + // can't clone the same index twice, target already exists + ResizeRequest repeatResizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + assertThatThrownBy(() -> restHighLevelClient.indices().clone(repeatResizeRequest, DEFAULT), statusException(BAD_REQUEST)); + } + } + + @Test + public void cloneIndex_negative() throws IOException { + // user cannot access target index + String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("clone_index_negative_source"); + String targetIndexName = "clone_index_negative_target"; + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + + assertThatThrownBy(() -> restHighLevelClient.indices().clone(resizeRequest, DEFAULT), statusException(FORBIDDEN)); + assertThat(cluster, not(indexExists(targetIndexName))); + } + + // user cannot access source index + sourceIndexName = "clone_index_negative_source"; + targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("clone_index_negative_target"); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + + assertThatThrownBy(() -> restHighLevelClient.indices().clone(resizeRequest, DEFAULT), statusException(FORBIDDEN)); + assertThat(cluster, not(indexExists(targetIndexName))); + } + } + + @Test + // required permissions: "indices:admin/resize", "indices:monitor/stats + public void splitIndex_positive() throws IOException { + String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("split_index_positive_source"); + Settings sourceIndexSettings = Settings.builder().put("index.blocks.write", true).build(); + String targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("split_index_positive_target"); + IndexOperationsHelper.createIndex(cluster, sourceIndexName, sourceIndexSettings); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + resizeRequest.setSettings(Settings.builder().put("index.number_of_shards", 2).build()); + ResizeResponse response = restHighLevelClient.indices().split(resizeRequest, DEFAULT); + + assertThat(response, isSuccessfulResizeResponse(targetIndexName)); + assertThat(cluster, indexExists(targetIndexName)); + } + } + + @Test + public void splitIndex_negative() throws IOException { + // user cannot access target index + String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("split_index_negative_source"); + String targetIndexName = "split_index_negative_target"; + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + resizeRequest.setSettings(Settings.builder().put("index.number_of_shards", 2).build()); + + assertThatThrownBy(() -> restHighLevelClient.indices().split(resizeRequest, DEFAULT), statusException(FORBIDDEN)); + assertThat(cluster, not(indexExists(targetIndexName))); + } + + // user cannot access source index + sourceIndexName = "split_index_negative_source"; + targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("split_index_negative_target"); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + resizeRequest.setSettings(Settings.builder().put("index.number_of_shards", 2).build()); + + assertThatThrownBy(() -> restHighLevelClient.indices().split(resizeRequest, DEFAULT), statusException(FORBIDDEN)); + assertThat(cluster, not(indexExists(targetIndexName))); + } + } + + @Test + // required permissions: "indices:monitor/settings/get" + public void getIndexSettings_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("get_index_settings_positive"); + IndexOperationsHelper.createIndex(cluster, indexName); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + GetSettingsRequest getSettingsRequest = new GetSettingsRequest().indices(indexName); + GetSettingsResponse response = restHighLevelClient.indices().getSettings(getSettingsRequest, DEFAULT); + + assertThat(response, getSettingsResponseContainsIndices(indexName)); + } + } + + @Test + public void getIndexSettings_negative() throws IOException { + String indexThatUserHasNoAccessTo = "get_index_settings_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + assertThatThrownBy( + () -> restHighLevelClient.indices().getSettings(new GetSettingsRequest().indices(indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .getSettings(new GetSettingsRequest().indices(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices().getSettings(new GetSettingsRequest().indices("*"), DEFAULT), + statusException(FORBIDDEN) + ); + } + } + + @Test + // required permissions: "indices:admin/settings/update" + public void updateIndexSettings_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("update_index_settings_positive"); + Settings initialSettings = Settings.builder().put("index.number_of_replicas", "2").build(); + Settings updatedSettings = Settings.builder().put("index.number_of_replicas", "4").build(); + IndexOperationsHelper.createIndex(cluster, indexName, initialSettings); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + UpdateSettingsRequest updateSettingsRequest = new UpdateSettingsRequest(indexName).settings(updatedSettings); + var response = restHighLevelClient.indices().putSettings(updateSettingsRequest, DEFAULT); + + assertThat(response.isAcknowledged(), is(true)); + assertThat(cluster, indexSettingsContainValues(indexName, updatedSettings)); + } + } + + @Test + public void updateIndexSettings_negative() throws IOException { + String indexThatUserHasNoAccessTo = "update_index_settings_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + Settings settingsToUpdate = Settings.builder().put("index.number_of_replicas", 2).build(); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + + assertThatThrownBy( + () -> restHighLevelClient.indices() + .putSettings(new UpdateSettingsRequest(indexThatUserHasNoAccessTo).settings(settingsToUpdate), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .putSettings( + new UpdateSettingsRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo).settings(settingsToUpdate), + DEFAULT + ), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices().putSettings(new UpdateSettingsRequest("*").settings(settingsToUpdate), DEFAULT), + statusException(FORBIDDEN) + ); + } + } + + @Test + // required permissions: indices:admin/mapping/put + public void createIndexMappings_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("create_index_mappings_positive"); + Map indexMapping = Map.of("properties", Map.of("message", Map.of("type", "text"))); + IndexOperationsHelper.createIndex(cluster, indexName); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + PutMappingRequest putMappingRequest = new PutMappingRequest(indexName).source(indexMapping); + var response = restHighLevelClient.indices().putMapping(putMappingRequest, DEFAULT); + + assertThat(response.isAcknowledged(), is(true)); + assertThat(cluster, indexMappingIsEqualTo(indexName, indexMapping)); + } + } + + @Test + public void createIndexMappings_negative() throws IOException { + String indexThatUserHasNoAccessTo = "create_index_mappings_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + Map indexMapping = Map.of("properties", Map.of("message", Map.of("type", "text"))); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + + assertThatThrownBy( + () -> restHighLevelClient.indices() + .putMapping(new PutMappingRequest(indexThatUserHasNoAccessTo).source(indexMapping), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .putMapping(new PutMappingRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo).source(indexMapping), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices().putMapping(new PutMappingRequest("*").source(indexMapping), DEFAULT), + statusException(FORBIDDEN) + ); + } + } + + @Test + // required permissions: indices:admin/mappings/get + public void getIndexMappings_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("get_index_mappings_positive"); + Map indexMapping = Map.of("properties", Map.of("message", Map.of("type", "text"))); + IndexOperationsHelper.createIndex(cluster, indexName); + IndexOperationsHelper.createMapping(cluster, indexName, indexMapping); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + GetMappingsRequest getMappingsRequest = new GetMappingsRequest().indices(indexName); + GetMappingsResponse response = restHighLevelClient.indices().getMapping(getMappingsRequest, DEFAULT); + + assertThat(response, getMappingsResponseContainsIndices(indexName)); + } + } + + @Test + public void getIndexMappings_negative() throws IOException { + String indexThatUserHasNoAccessTo = "get_index_mappings_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + + assertThatThrownBy( + () -> restHighLevelClient.indices().getMapping(new GetMappingsRequest().indices(indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .getMapping(new GetMappingsRequest().indices(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices().getMapping(new GetMappingsRequest().indices("*"), DEFAULT), + statusException(FORBIDDEN) + ); + } + } + + @Test + // required permissions: "indices:admin/cache/clear" + public void clearIndexCache_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("clear_index_cache_positive"); + IndexOperationsHelper.createIndex(cluster, indexName); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ClearIndicesCacheRequest clearIndicesCacheRequest = new ClearIndicesCacheRequest(indexName); + ClearIndicesCacheResponse response = restHighLevelClient.indices().clearCache(clearIndicesCacheRequest, DEFAULT); + + assertThat(response, isSuccessfulClearIndicesCacheResponse()); + } + } + + @Test + public void clearIndexCache_negative() throws IOException { + String indexThatUserHasNoAccessTo = "clear_index_cache_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + + assertThatThrownBy( + () -> restHighLevelClient.indices().clearCache(new ClearIndicesCacheRequest(indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .clearCache(new ClearIndicesCacheRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices().clearCache(new ClearIndicesCacheRequest("*"), DEFAULT), + statusException(FORBIDDEN) + ); + } + } + + @Test + // required permissions: "indices:admin/create", "indices:admin/aliases" + public void shouldCreateIndexWithAlias_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("create_index_with_alias_positive"); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName).alias( + new Alias(ALIAS_CREATE_INDEX_WITH_ALIAS_POSITIVE) + ); + + CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(createIndexRequest, DEFAULT); + + assertThat(createIndexResponse, isSuccessfulCreateIndexResponse(indexName)); + assertThat(cluster, indexExists(indexName)); + assertThat(internalClient, aliasExists(ALIAS_CREATE_INDEX_WITH_ALIAS_POSITIVE)); + } + auditLogsRule.assertExactlyOne( + userAuthenticated(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES).withRestRequest( + PUT, + "/index_operations_create_index_with_alias_positive" + ) + ); + auditLogsRule.assertExactly( + 1, + grantedPrivilege(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES, "CreateIndexRequest") + ); + auditLogsRule.assertExactly( + 1, + auditPredicate(INDEX_EVENT).withEffectiveUser(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES) + ); + } + + @Test + public void shouldCreateIndexWithAlias_negative() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("create_index_with_alias_negative"); + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_CREATE_INDEX)) { + CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName).alias( + new Alias(ALIAS_CREATE_INDEX_WITH_ALIAS_NEGATIVE) + ); + + assertThatThrownBy(() -> restHighLevelClient.indices().create(createIndexRequest, DEFAULT), statusException(FORBIDDEN)); + + assertThat(internalClient, not(aliasExists(ALIAS_CREATE_INDEX_WITH_ALIAS_NEGATIVE))); + } + auditLogsRule.assertExactlyOne( + userAuthenticated(USER_ALLOWED_TO_CREATE_INDEX).withRestRequest(PUT, "/index_operations_create_index_with_alias_negative") + ); + auditLogsRule.assertExactlyOne(missingPrivilege(USER_ALLOWED_TO_CREATE_INDEX, "CreateIndexRequest")); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/legacy/SecurityRolesTests.java b/src/integrationTest/java/org/opensearch/security/legacy/SecurityRolesTests.java new file mode 100644 index 0000000000..15cb0b7455 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/legacy/SecurityRolesTests.java @@ -0,0 +1,67 @@ +/* +* 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. +* +* Modifications Copyright OpenSearch Contributors. See +* GitHub history for details. +*/ + +package org.opensearch.security.legacy; + +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.HttpStatus; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class SecurityRolesTests { + + protected final static TestSecurityConfig.User USER_SR = new TestSecurityConfig.User("sr_user").roles( + new Role("abc_ber").indexPermissions("*").on("*").clusterPermissions("*"), + new Role("def_efg").indexPermissions("*").on("*").clusterPermissions("*") + ); + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .anonymousAuth(true) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USER_SR) + .nodeSettings(Map.of(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), true)) + .build(); + + @Test + public void testSecurityRoles() throws Exception { + try (TestRestClient client = cluster.getRestClient(USER_SR)) { + HttpResponse response = client.getAuthInfo(); + response.assertStatusCode(HttpStatus.SC_OK); + + // Check username + assertThat(response.getTextFromJsonBody("/user_name"), equalTo("sr_user")); + + // Check security roles + assertThat(response.getTextFromJsonBody("/roles/0"), equalTo("user_sr_user__abc_ber")); + assertThat(response.getTextFromJsonBody("/roles/1"), equalTo("user_sr_user__def_efg")); + + } + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/legacy/SystemIndexTests.java b/src/integrationTest/java/org/opensearch/security/legacy/SystemIndexTests.java new file mode 100644 index 0000000000..8a1e7a03ee --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/legacy/SystemIndexTests.java @@ -0,0 +1,119 @@ +/* + * Copyright OpenSearch Contributors + * 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.security.legacy; + +import java.util.List; +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.core.rest.RestStatus; +import org.opensearch.security.http.ExampleSystemIndexPlugin; +import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED; +import static org.opensearch.security.support.ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class SystemIndexTests { + + public static final AuthcDomain AUTHC_DOMAIN = new AuthcDomain("basic", 0).httpAuthenticatorWithChallenge("basic").backend("internal"); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .authc(AUTHC_DOMAIN) + .users(USER_ADMIN) + .plugin(ExampleSystemIndexPlugin.class) + .nodeSettings( + Map.of( + SECURITY_RESTAPI_ROLES_ENABLED, + List.of("user_" + USER_ADMIN.getName() + "__" + ALL_ACCESS.getName()), + SECURITY_SYSTEM_INDICES_ENABLED_KEY, + true, + PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), + true + ) + ) + .build(); + + @Before + public void setup() { + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + client.delete(".system-index1"); + } + } + + @Test + public void adminShouldNotBeAbleToDeleteSecurityIndex() { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.delete(".opendistro_security"); + + assertThat(response.getStatusCode(), equalTo(RestStatus.FORBIDDEN.getStatus())); + + // Create regular index + client.put("test-index"); + + // regular user can delete non-system index + HttpResponse response2 = client.delete("test-index"); + + assertThat(response2.getStatusCode(), equalTo(RestStatus.OK.getStatus())); + + // regular use can create system index + HttpResponse response3 = client.put(".system-index1"); + + assertThat(response3.getStatusCode(), equalTo(RestStatus.OK.getStatus())); + + // regular user cannot delete system index + HttpResponse response4 = client.delete(".system-index1"); + + assertThat(response4.getStatusCode(), equalTo(RestStatus.FORBIDDEN.getStatus())); + } + } + + @Test + public void regularUserShouldGetNoResultsWhenSearchingSystemIndex() { + // Create system index and index a dummy document as the super admin user, data returned to super admin + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + HttpResponse response1 = client.put(".system-index1"); + + assertThat(response1.getStatusCode(), equalTo(RestStatus.OK.getStatus())); + String doc = "{\"field\":\"value\"}"; + HttpResponse adminPostResponse = client.postJson(".system-index1/_doc/1?refresh=true", doc); + assertThat(adminPostResponse.getStatusCode(), equalTo(RestStatus.CREATED.getStatus())); + HttpResponse response2 = client.get(".system-index1/_search"); + + assertThat(response2.getStatusCode(), equalTo(RestStatus.OK.getStatus())); + assertThat(response2.getBody(), response2.getBody().contains("\"hits\":{\"total\":{\"value\":1,\"relation\":\"eq\"}")); + } + + // Regular users should not be able to read it + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + // regular user cannot read system index + HttpResponse response1 = client.get(".system-index1/_search"); + + assertThat(response1.getBody(), response1.getBody().contains("\"hits\":{\"total\":{\"value\":0,\"relation\":\"eq\"}")); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java new file mode 100644 index 0000000000..7807dae748 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/ActionPrivilegesTest.java @@ -0,0 +1,1033 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.security.privileges; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Suite; + +import org.opensearch.action.support.IndicesOptions; +import org.opensearch.cluster.metadata.IndexAbstraction; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.common.unit.ByteSizeUnit; +import org.opensearch.core.common.unit.ByteSizeValue; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.securityconf.FlattenedActionGroups; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.RoleV7; +import org.opensearch.security.user.User; +import org.opensearch.security.util.MockIndexMetadataBuilder; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isAllowed; +import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isForbidden; +import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.isPartiallyOk; +import static org.opensearch.security.privileges.PrivilegeEvaluatorResponseMatcher.missingPrivileges; +import static org.opensearch.security.util.MockIndexMetadataBuilder.dataStreams; +import static org.opensearch.security.util.MockIndexMetadataBuilder.indices; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for ActionPrivileges. As the ActionPrivileges provides quite a few different code paths for checking + * privileges with different performance characteristics, this test suite defines different test cases for making sure + * all these code paths are tested. So, all functionality must be tested for "well-known" actions and non-well-known + * actions. For index privileges, there are a couple of more tests dimensions. See below. + */ +@RunWith(Suite.class) +@Suite.SuiteClasses({ + ActionPrivilegesTest.ClusterPrivileges.class, + ActionPrivilegesTest.IndexPrivileges.IndicesAndAliases.class, + ActionPrivilegesTest.IndexPrivileges.DataStreams.class, + ActionPrivilegesTest.Misc.class, + ActionPrivilegesTest.StatefulIndexPrivilegesHeapSize.class }) +public class ActionPrivilegesTest { + public static class ClusterPrivileges { + @Test + public void wellKnown() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - cluster:monitor/nodes/stats*", CType.ROLES); + + ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + + assertThat(subject.hasClusterPrivilege(ctx("test_role"), "cluster:monitor/nodes/stats"), isAllowed()); + assertThat( + subject.hasClusterPrivilege(ctx("other_role"), "cluster:monitor/nodes/stats"), + isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) + ); + assertThat( + subject.hasClusterPrivilege(ctx("test_role"), "cluster:monitor/nodes/other"), + isForbidden(missingPrivileges("cluster:monitor/nodes/other")) + ); + } + + @Test + public void notWellKnown() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - cluster:monitor/nodes/stats*", CType.ROLES); + + ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + + assertThat(subject.hasClusterPrivilege(ctx("test_role"), "cluster:monitor/nodes/stats/somethingnotwellknown"), isAllowed()); + assertThat( + subject.hasClusterPrivilege(ctx("other_role"), "cluster:monitor/nodes/stats/somethingnotwellknown"), + isForbidden(missingPrivileges("cluster:monitor/nodes/stats/somethingnotwellknown")) + ); + assertThat( + subject.hasClusterPrivilege(ctx("test_role"), "cluster:monitor/nodes/something/else"), + isForbidden(missingPrivileges("cluster:monitor/nodes/something/else")) + ); + } + + @Test + public void wildcard() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - '*'", CType.ROLES); + + ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + + assertThat(subject.hasClusterPrivilege(ctx("test_role"), "cluster:whatever"), isAllowed()); + assertThat( + subject.hasClusterPrivilege(ctx("other_role"), "cluster:whatever"), + isForbidden(missingPrivileges("cluster:whatever")) + ); + } + + @Test + public void explicit_wellKnown() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("non_explicit_role:\n" + // + " cluster_permissions:\n" + // + " - '*'\n" + // + "explicit_role:\n" + // + " cluster_permissions:\n" + // + " - cluster:monitor/nodes/stats\n" + // + "semi_explicit_role:\n" + // + " cluster_permissions:\n" + // + " - cluster:monitor/nodes/stats*\n", // + CType.ROLES + ); + + ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + + assertThat(subject.hasExplicitClusterPrivilege(ctx("explicit_role"), "cluster:monitor/nodes/stats"), isAllowed()); + assertThat(subject.hasExplicitClusterPrivilege(ctx("semi_explicit_role"), "cluster:monitor/nodes/stats"), isAllowed()); + assertThat( + subject.hasExplicitClusterPrivilege(ctx("non_explicit_role"), "cluster:monitor/nodes/stats"), + isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) + ); + assertThat( + subject.hasExplicitClusterPrivilege(ctx("other_role"), "cluster:monitor/nodes/stats"), + isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) + ); + } + + @Test + public void explicit_notWellKnown() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("non_explicit_role:\n" + // + " cluster_permissions:\n" + // + " - '*'\n" + // + "explicit_role:\n" + // + " cluster_permissions:\n" + // + " - cluster:monitor/nodes/notwellknown\n" + // + "semi_explicit_role:\n" + // + " cluster_permissions:\n" + // + " - cluster:monitor/nodes/*\n", // + CType.ROLES + ); + + ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + + assertThat(subject.hasExplicitClusterPrivilege(ctx("explicit_role"), "cluster:monitor/nodes/notwellknown"), isAllowed()); + assertThat(subject.hasExplicitClusterPrivilege(ctx("semi_explicit_role"), "cluster:monitor/nodes/notwellknown"), isAllowed()); + assertThat( + subject.hasExplicitClusterPrivilege(ctx("non_explicit_role"), "cluster:monitor/nodes/notwellknown"), + isForbidden(missingPrivileges("cluster:monitor/nodes/notwellknown")) + ); + assertThat( + subject.hasExplicitClusterPrivilege(ctx("other_role"), "cluster:monitor/nodes/notwellknown"), + isForbidden(missingPrivileges("cluster:monitor/nodes/notwellknown")) + ); + } + + @Test + public void hasAny_wellKnown() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - cluster:monitor/nodes/stats*", CType.ROLES); + + ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + + assertThat(subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/nodes/stats")), isAllowed()); + assertThat( + subject.hasAnyClusterPrivilege( + ctx("test_role"), + ImmutableSet.of("cluster:monitor/nodes/foo", "cluster:monitor/nodes/stats") + ), + isAllowed() + ); + + assertThat( + subject.hasAnyClusterPrivilege(ctx("other_role"), ImmutableSet.of("cluster:monitor/nodes/stats")), + isForbidden(missingPrivileges("cluster:monitor/nodes/stats")) + ); + assertThat( + subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/nodes/other")), + isForbidden(missingPrivileges("cluster:monitor/nodes/other")) + ); + } + + @Test + public void hasAny_notWellKnown() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - cluster:monitor/nodes/*", CType.ROLES); + + ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + + assertThat( + subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/nodes/notwellknown")), + isAllowed() + ); + assertThat( + subject.hasAnyClusterPrivilege( + ctx("test_role"), + ImmutableSet.of("cluster:monitor/other", "cluster:monitor/nodes/notwellknown") + ), + isAllowed() + ); + + assertThat( + subject.hasAnyClusterPrivilege(ctx("other_role"), ImmutableSet.of("cluster:monitor/nodes/notwellknown")), + isForbidden(missingPrivileges("cluster:monitor/nodes/notwellknown")) + ); + assertThat( + subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/other")), + isForbidden(missingPrivileges("cluster:monitor/other")) + ); + assertThat( + subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:monitor/other", "cluster:monitor/yetanother")), + isForbidden() + ); + } + + @Test + public void hasAny_wildcard() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - '*'", CType.ROLES); + + ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + + assertThat(subject.hasAnyClusterPrivilege(ctx("test_role"), ImmutableSet.of("cluster:whatever")), isAllowed()); + + assertThat( + subject.hasAnyClusterPrivilege(ctx("other_role"), ImmutableSet.of("cluster:whatever")), + isForbidden(missingPrivileges("cluster:whatever")) + ); + } + } + + /** + * Tests for index privileges. This class contains two parameterized test suites, first for indices and aliases, + * second for data streams. + *

+ * Both test suites use parameters to create a 3-dimensional test case space to make sure all code paths are covered. + *

+ * The dimensions are (see also the params() methods): + *

    + *
  1. 1. roles.yml; index patterns: Different usages of patterns, wildcards and constant names. + *
  2. 2. roles.yml; action patterns: Well known actions vs non-well known actions combined with use of patterns vs use of constant action names + *
  3. 3. Statefulness: Shall the data structures from ActionPrivileges.StatefulIndexPrivileges be used or not + *
+ * As so many different situations need to be tested, the test oracle method covers() is used to verify the results. + */ + public static class IndexPrivileges { + + @RunWith(Parameterized.class) + public static class IndicesAndAliases { + final ActionSpec actionSpec; + final IndexSpec indexSpec; + final SecurityDynamicConfiguration roles; + final String primaryAction; + final ImmutableSet requiredActions; + final ImmutableSet otherActions; + final ActionPrivileges subject; + + @Test + public void positive_full() throws Exception { + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx("test_role"), requiredActions, resolved("index_a11")); + assertThat(result, isAllowed()); + } + + @Test + public void positive_partial() throws Exception { + PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("index_a11", "index_a12")); + + if (covers(ctx, "index_a11", "index_a12")) { + assertThat(result, isAllowed()); + } else if (covers(ctx, "index_a11")) { + assertThat(result, isPartiallyOk("index_a11")); + } else { + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + } + } + + @Test + public void positive_partial2() throws Exception { + PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( + ctx, + requiredActions, + resolved("index_a11", "index_a12", "index_b1") + ); + + if (covers(ctx, "index_a11", "index_a12", "index_b1")) { + assertThat(result, isAllowed()); + } else if (covers(ctx, "index_a11", "index_a12")) { + assertThat(result, isPartiallyOk("index_a11", "index_a12")); + } else if (covers(ctx, "index_a11")) { + assertThat(result, isPartiallyOk("index_a11")); + } else { + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + } + } + + @Test + public void positive_noLocal() throws Exception { + IndexResolverReplacer.Resolved resolved = new IndexResolverReplacer.Resolved( + ImmutableSet.of(), + ImmutableSet.of(), + ImmutableSet.of("remote:a"), + ImmutableSet.of("remote:a"), + IndicesOptions.LENIENT_EXPAND_OPEN + ); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx("test_role"), requiredActions, resolved); + assertThat(result, isAllowed()); + } + + @Test + public void negative_wrongRole() throws Exception { + PrivilegesEvaluationContext ctx = ctx("other_role"); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("index_a11")); + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + } + + @Test + public void negative_wrongAction() throws Exception { + PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, otherActions, resolved("index_a11")); + + if (actionSpec.givenPrivs.contains("*")) { + assertThat(result, isAllowed()); + } else { + assertThat(result, isForbidden(missingPrivileges(otherActions))); + } + } + + @Test + public void positive_hasExplicit_full() { + PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege(ctx, requiredActions, resolved("index_a11")); + + if (actionSpec.givenPrivs.contains("*")) { + // The * is forbidden for explicit privileges + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + } else if (!requiredActions.contains("indices:data/read/search")) { + // For test purposes, we have designated "indices:data/read/search" as an action requiring explicit privileges + // Other actions are not covered here + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + } else { + assertThat(result, isAllowed()); + } + } + + private boolean covers(PrivilegesEvaluationContext ctx, String... indices) { + for (String index : indices) { + if (!indexSpec.covers(ctx.getUser(), index)) { + return false; + } + } + return true; + } + + @Parameterized.Parameters(name = "{0}; actions: {1}; {2}") + public static Collection params() { + List result = new ArrayList<>(); + + for (IndexSpec indexSpec : Arrays.asList( + new IndexSpec().givenIndexPrivs("*"), // + new IndexSpec().givenIndexPrivs("index_*"), // + new IndexSpec().givenIndexPrivs("index_a11"), // + new IndexSpec().givenIndexPrivs("index_a1*"), // + new IndexSpec().givenIndexPrivs("index_${attrs.dept_no}"), // + new IndexSpec().givenIndexPrivs("alias_a1*") // + )) { + for (ActionSpec actionSpec : Arrays.asList( + new ActionSpec("wildcard")// + .givenPrivs("*") + .requiredPrivs("indices:data/read/search"), // + new ActionSpec("constant, well known")// + .givenPrivs("indices:data/read/search") + .requiredPrivs("indices:data/read/search"), // + new ActionSpec("pattern, well known")// + .givenPrivs("indices:data/read/*") + .requiredPrivs("indices:data/read/search"), // + new ActionSpec("pattern, well known, two required privs")// + .givenPrivs("indices:data/read/*") + .requiredPrivs("indices:data/read/search", "indices:data/read/get"), // + new ActionSpec("constant, non well known")// + .givenPrivs("indices:unknown/unwell") + .requiredPrivs("indices:unknown/unwell"), // + new ActionSpec("pattern, non well known")// + .givenPrivs("indices:unknown/*") + .requiredPrivs("indices:unknown/unwell"), // + new ActionSpec("pattern, non well known, two required privs")// + .givenPrivs("indices:unknown/*") + .requiredPrivs("indices:unknown/unwell", "indices:unknown/notatall")// + + )) { + for (Statefulness statefulness : Statefulness.values()) { + result.add(new Object[] { indexSpec, actionSpec, statefulness }); + } + } + } + return result; + } + + public IndicesAndAliases(IndexSpec indexSpec, ActionSpec actionSpec, Statefulness statefulness) throws Exception { + this.indexSpec = indexSpec; + this.actionSpec = actionSpec; + this.roles = indexSpec.toRolesConfig(actionSpec); + + this.primaryAction = actionSpec.primaryAction; + this.requiredActions = actionSpec.requiredPrivs; + + this.otherActions = actionSpec.wellKnownActions + ? ImmutableSet.of("indices:data/write/update") + : ImmutableSet.of("indices:foobar/unknown"); + this.indexSpec.indexMetadata = INDEX_METADATA; + + Settings settings = Settings.EMPTY; + if (statefulness == Statefulness.STATEFUL_LIMITED) { + settings = Settings.builder() + .put(ActionPrivileges.PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.getKey(), new ByteSizeValue(10, ByteSizeUnit.BYTES)) + .build(); + } + + this.subject = new ActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + () -> INDEX_METADATA, + settings, + WellKnownActions.CLUSTER_ACTIONS, + WellKnownActions.INDEX_ACTIONS, + WellKnownActions.INDEX_ACTIONS + ); + + if (statefulness == Statefulness.STATEFUL || statefulness == Statefulness.STATEFUL_LIMITED) { + this.subject.updateStatefulIndexPrivileges(INDEX_METADATA, 1); + } + } + + final static Map INDEX_METADATA = // + indices("index_a11", "index_a12", "index_a21", "index_a22", "index_b1", "index_b2")// + .alias("alias_a") + .of("index_a11", "index_a12", "index_a21", "index_a22")// + .alias("alias_a1") + .of("index_a11", "index_a12")// + .alias("alias_a2") + .of("index_a21", "index_a22")// + .alias("alias_b") + .of("index_b1", "index_b2")// + .build() + .getIndicesLookup(); + + static IndexResolverReplacer.Resolved resolved(String... indices) { + return new IndexResolverReplacer.Resolved( + ImmutableSet.of(), + ImmutableSet.copyOf(indices), + ImmutableSet.copyOf(indices), + ImmutableSet.of(), + IndicesOptions.LENIENT_EXPAND_OPEN + ); + } + } + + @RunWith(Parameterized.class) + public static class DataStreams { + final ActionSpec actionSpec; + final IndexSpec indexSpec; + final SecurityDynamicConfiguration roles; + final String primaryAction; + final ImmutableSet requiredActions; + final ImmutableSet otherActions; + final ActionPrivileges subject; + + @Test + public void positive_full() throws Exception { + PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("data_stream_a11")); + if (covers(ctx, "data_stream_a11")) { + assertThat(result, isAllowed()); + } else if (covers(ctx, ".ds-data_stream_a11-000001")) { + assertThat( + result, + isPartiallyOk(".ds-data_stream_a11-000001", ".ds-data_stream_a11-000002", ".ds-data_stream_a11-000003") + ); + } else { + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + } + } + + @Test + public void positive_partial() throws Exception { + PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( + ctx, + requiredActions, + resolved("data_stream_a11", "data_stream_a12") + ); + + if (covers(ctx, "data_stream_a11", "data_stream_a12")) { + assertThat(result, isAllowed()); + } else if (covers(ctx, "data_stream_a11")) { + assertThat( + result, + isPartiallyOk( + "data_stream_a11", + ".ds-data_stream_a11-000001", + ".ds-data_stream_a11-000002", + ".ds-data_stream_a11-000003" + ) + ); + } else if (covers(ctx, ".ds-data_stream_a11-000001")) { + assertThat( + result, + isPartiallyOk(".ds-data_stream_a11-000001", ".ds-data_stream_a11-000002", ".ds-data_stream_a11-000003") + ); + } else { + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + } + } + + @Test + public void negative_wrongRole() throws Exception { + PrivilegesEvaluationContext ctx = ctx("other_role"); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, requiredActions, resolved("data_stream_a11")); + assertThat(result, isForbidden(missingPrivileges(requiredActions))); + } + + @Test + public void negative_wrongAction() throws Exception { + PrivilegesEvaluationContext ctx = ctx("test_role"); + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege(ctx, otherActions, resolved("data_stream_a11")); + assertThat(result, isForbidden(missingPrivileges(otherActions))); + } + + private boolean covers(PrivilegesEvaluationContext ctx, String... indices) { + for (String index : indices) { + if (!indexSpec.covers(ctx.getUser(), index)) { + return false; + } + } + return true; + } + + @Parameterized.Parameters(name = "{0}; actions: {1}; {2}") + public static Collection params() { + List result = new ArrayList<>(); + + for (IndexSpec indexSpec : Arrays.asList( + new IndexSpec().givenIndexPrivs("*"), // + new IndexSpec().givenIndexPrivs("data_stream_*"), // + new IndexSpec().givenIndexPrivs("data_stream_a11"), // + new IndexSpec().givenIndexPrivs("data_stream_a1*"), // + new IndexSpec().givenIndexPrivs("data_stream_${attrs.dept_no}"), // + new IndexSpec().givenIndexPrivs(".ds-data_stream_a11*") // + )) { + for (ActionSpec actionSpec : Arrays.asList( + new ActionSpec("constant, well known")// + .givenPrivs("indices:data/read/search") + .requiredPrivs("indices:data/read/search"), // + new ActionSpec("pattern, well known")// + .givenPrivs("indices:data/read/*") + .requiredPrivs("indices:data/read/search"), // + new ActionSpec("pattern, well known, two required privs")// + .givenPrivs("indices:data/read/*") + .requiredPrivs("indices:data/read/search", "indices:data/read/get"), // + new ActionSpec("constant, non well known")// + .givenPrivs("indices:unknown/unwell") + .requiredPrivs("indices:unknown/unwell"), // + new ActionSpec("pattern, non well known")// + .givenPrivs("indices:unknown/*") + .requiredPrivs("indices:unknown/unwell"), // + new ActionSpec("pattern, non well known, two required privs")// + .givenPrivs("indices:unknown/*") + .requiredPrivs("indices:unknown/unwell", "indices:unknown/notatall")// + + )) { + for (Statefulness statefulness : Statefulness.values()) { + result.add(new Object[] { indexSpec, actionSpec, statefulness }); + } + } + } + return result; + } + + public DataStreams(IndexSpec indexSpec, ActionSpec actionSpec, Statefulness statefulness) throws Exception { + this.indexSpec = indexSpec; + this.actionSpec = actionSpec; + this.roles = indexSpec.toRolesConfig(actionSpec); + + this.primaryAction = actionSpec.primaryAction; + this.requiredActions = actionSpec.requiredPrivs; + + this.otherActions = actionSpec.wellKnownActions + ? ImmutableSet.of("indices:data/write/update") + : ImmutableSet.of("indices:foobar/unknown"); + this.indexSpec.indexMetadata = INDEX_METADATA; + + Settings settings = Settings.EMPTY; + if (statefulness == Statefulness.STATEFUL_LIMITED) { + settings = Settings.builder() + .put(ActionPrivileges.PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.getKey(), new ByteSizeValue(10, ByteSizeUnit.BYTES)) + .build(); + } + + this.subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, () -> INDEX_METADATA, settings); + + if (statefulness == Statefulness.STATEFUL || statefulness == Statefulness.STATEFUL_LIMITED) { + this.subject.updateStatefulIndexPrivileges(INDEX_METADATA, 1); + } + } + + final static Map INDEX_METADATA = // + dataStreams("data_stream_a11", "data_stream_a12", "data_stream_a21", "data_stream_a22", "data_stream_b1", "data_stream_b2") + .build() + .getIndicesLookup(); + + static IndexResolverReplacer.Resolved resolved(String... indices) { + ImmutableSet.Builder allIndices = ImmutableSet.builder(); + + for (String index : indices) { + IndexAbstraction indexAbstraction = INDEX_METADATA.get(index); + + if (indexAbstraction instanceof IndexAbstraction.DataStream) { + allIndices.addAll( + indexAbstraction.getIndices().stream().map(i -> i.getIndex().getName()).collect(Collectors.toList()) + ); + } + + allIndices.add(index); + } + + return new IndexResolverReplacer.Resolved( + ImmutableSet.of(), + allIndices.build(), + ImmutableSet.copyOf(indices), + ImmutableSet.of(), + IndicesOptions.LENIENT_EXPAND_OPEN + ); + } + } + + static class IndexSpec { + ImmutableList givenIndexPrivs = ImmutableList.of(); + boolean wildcardPrivs; + Map indexMetadata; + + IndexSpec() {} + + IndexSpec givenIndexPrivs(String... indexPatterns) { + this.givenIndexPrivs = ImmutableList.copyOf(indexPatterns); + this.wildcardPrivs = this.givenIndexPrivs.contains("*"); + return this; + } + + boolean covers(User user, String index) { + if (this.wildcardPrivs) { + return true; + } + + for (String givenIndexPriv : this.givenIndexPrivs) { + if (givenIndexPriv.contains("${")) { + for (Map.Entry entry : user.getCustomAttributesMap().entrySet()) { + givenIndexPriv = givenIndexPriv.replace("${" + entry.getKey() + "}", entry.getValue()); + } + } + + if (givenIndexPriv.endsWith("*")) { + if (index.startsWith(givenIndexPriv.substring(0, givenIndexPriv.length() - 1))) { + return true; + } + + for (IndexAbstraction indexAbstraction : indexMetadata.values()) { + if ((indexAbstraction instanceof IndexAbstraction.Alias + || indexAbstraction instanceof IndexAbstraction.DataStream) + && indexAbstraction.getName().startsWith(givenIndexPriv.substring(0, givenIndexPriv.length() - 1))) { + if (indexAbstraction.getIndices().stream().anyMatch(i -> i.getIndex().getName().equals(index))) { + return true; + } + } + } + } else if (givenIndexPrivs.contains("*")) { + // For simplicity, we only allow a sub-set of patterns. We assume here that the WildcardMatcher + // class fulfills all other cases correctly as per its contract + throw new RuntimeException("The tests only support index patterns with * at the end"); + } else { + if (index.equals(givenIndexPriv)) { + return true; + } + + IndexAbstraction indexAbstraction = indexMetadata.get(index); + + if (indexAbstraction instanceof IndexAbstraction.Alias || indexAbstraction instanceof IndexAbstraction.DataStream) { + if (indexAbstraction.getIndices().stream().anyMatch(i -> i.getIndex().getName().equals(index))) { + return true; + } + } + } + } + + return false; + } + + SecurityDynamicConfiguration toRolesConfig(ActionSpec actionSpec) { + try { + return SecurityDynamicConfiguration.fromMap( + ImmutableMap.of( + "test_role", + ImmutableMap.of( + "index_permissions", + Arrays.asList( + ImmutableMap.of("index_patterns", this.givenIndexPrivs, "allowed_actions", actionSpec.givenPrivs) + ) + ) + ), + CType.ROLES + ); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + @Override + public String toString() { + return this.givenIndexPrivs.stream().collect(Collectors.joining(",")); + } + } + + static class ActionSpec { + String name; + ImmutableList givenPrivs; + ImmutableSet requiredPrivs; + String primaryAction; + boolean wellKnownActions; + + ActionSpec(String name) { + super(); + this.name = name; + } + + ActionSpec givenPrivs(String... actions) { + this.givenPrivs = ImmutableList.copyOf(actions); + return this; + } + + ActionSpec requiredPrivs(String... requiredPrivs) { + this.requiredPrivs = ImmutableSet.copyOf(requiredPrivs); + this.primaryAction = requiredPrivs[0]; + this.wellKnownActions = this.requiredPrivs.stream().anyMatch(a -> WellKnownActions.INDEX_ACTIONS.contains(a)); + return this; + } + + @Override + public String toString() { + return name; + } + } + + enum Statefulness { + STATEFUL, + STATEFUL_LIMITED, + NON_STATEFUL + } + } + + public static class Misc { + @Test + public void relevantOnly_identity() throws Exception { + Map metadata = // + indices("index_a11", "index_a12", "index_b")// + .alias("alias_a") + .of("index_a11", "index_a12")// + .build() + .getIndicesLookup(); + + assertTrue( + "relevantOnly() returned identical object", + ActionPrivileges.StatefulIndexPrivileges.relevantOnly(metadata) == metadata + ); + } + + @Test + public void relevantOnly_closed() throws Exception { + Map metadata = indices("index_open_1", "index_open_2")// + .index("index_closed", IndexMetadata.State.CLOSE) + .build() + .getIndicesLookup(); + + assertNotNull("Original metadata contains index_open_1", metadata.get("index_open_1")); + assertNotNull("Original metadata contains index_closed", metadata.get("index_closed")); + + Map filteredMetadata = ActionPrivileges.StatefulIndexPrivileges.relevantOnly(metadata); + + assertNotNull("Filtered metadata contains index_open_1", filteredMetadata.get("index_open_1")); + assertNull("Filtered metadata does not contain index_closed", filteredMetadata.get("index_closed")); + } + + @Test + public void relevantOnly_dataStreamBackingIndices() throws Exception { + Map metadata = dataStreams("data_stream_1").build().getIndicesLookup(); + + assertNotNull("Original metadata contains backing index", metadata.get(".ds-data_stream_1-000001")); + assertNotNull("Original metadata contains data stream", metadata.get("data_stream_1")); + + Map filteredMetadata = ActionPrivileges.StatefulIndexPrivileges.relevantOnly(metadata); + + assertNull("Filtered metadata does not contain backing index", filteredMetadata.get(".ds-data_stream_1-000001")); + assertNotNull("Filtered metadata contains data stream", filteredMetadata.get("data_stream_1")); + } + + @Test + public void backingIndexToDataStream() { + Map metadata = indices("index").dataStream("data_stream").build().getIndicesLookup(); + + assertEquals("index", ActionPrivileges.StatefulIndexPrivileges.backingIndexToDataStream("index", metadata)); + assertEquals( + "data_stream", + ActionPrivileges.StatefulIndexPrivileges.backingIndexToDataStream(".ds-data_stream-000001", metadata) + ); + assertEquals("non_existing", ActionPrivileges.StatefulIndexPrivileges.backingIndexToDataStream("non_existing", metadata)); + } + + @Test + public void hasIndexPrivilege_errors() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml( + "role_with_errors:\n" + + " index_permissions:\n" + + " - index_patterns: ['/invalid_regex_with_attr${user.name}\\/']\n" + + " allowed_actions: ['indices:some_action*', 'indices:data/write/index']", + CType.ROLES + ); + + ActionPrivileges subject = new ActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + () -> Collections.emptyMap(), + Settings.EMPTY + ); + + PrivilegesEvaluatorResponse result = subject.hasIndexPrivilege( + ctx("role_with_errors"), + Set.of("indices:some_action", "indices:data/write/index"), + IndexResolverReplacer.Resolved.ofIndex("any_index") + ); + assertThat(result, isForbidden()); + assertTrue(result.hasEvaluationExceptions()); + assertTrue( + "Result mentions role_with_errors: " + result.getEvaluationExceptionInfo(), + result.getEvaluationExceptionInfo() + .startsWith("Exceptions encountered during privilege evaluation:\n" + "Error while evaluating role role_with_errors") + ); + } + + @Test + public void hasExplicitIndexPrivilege_errors() throws Exception { + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml( + "role_with_errors:\n" + + " index_permissions:\n" + + " - index_patterns: ['/invalid_regex_with_attr${user.name}\\/']\n" + + " allowed_actions: ['system:admin/system_index*']", + CType.ROLES + ); + + ActionPrivileges subject = new ActionPrivileges( + roles, + FlattenedActionGroups.EMPTY, + () -> Collections.emptyMap(), + Settings.EMPTY + ); + + PrivilegesEvaluatorResponse result = subject.hasExplicitIndexPrivilege( + ctx("role_with_errors"), + Set.of("system:admin/system_index"), + IndexResolverReplacer.Resolved.ofIndex("any_index") + ); + assertThat(result, isForbidden()); + assertTrue(result.hasEvaluationExceptions()); + assertTrue( + "Result mentions role_with_errors: " + result.getEvaluationExceptionInfo(), + result.getEvaluationExceptionInfo() + .startsWith("Exceptions encountered during privilege evaluation:\n" + "Error while evaluating role role_with_errors") + ); + } + } + + /** + * Verifies that the heap size used by StatefulIndexPrivileges stays within expected bounds. + */ + @RunWith(Parameterized.class) + public static class StatefulIndexPrivilegesHeapSize { + + final Map indices; + final SecurityDynamicConfiguration roles; + final int expectedEstimatedNumberOfBytes; + + @Test + public void estimatedSize() throws Exception { + ActionPrivileges subject = new ActionPrivileges(roles, FlattenedActionGroups.EMPTY, () -> indices, Settings.EMPTY); + + subject.updateStatefulIndexPrivileges(indices, 1); + + int lowerBound = (int) (expectedEstimatedNumberOfBytes * 0.9); + int upperBound = (int) (expectedEstimatedNumberOfBytes * 1.1); + + int actualEstimatedNumberOfBytes = subject.getEstimatedStatefulIndexByteSize(); + + assertTrue( + "estimatedNumberOfBytes: " + lowerBound + " < " + actualEstimatedNumberOfBytes + " < " + upperBound, + lowerBound < actualEstimatedNumberOfBytes && actualEstimatedNumberOfBytes < upperBound + ); + } + + public StatefulIndexPrivilegesHeapSize(int numberOfIndices, int numberOfRoles, int expectedEstimatedNumberOfBytes) { + this.indices = createIndices(numberOfIndices); + this.roles = createRoles(numberOfRoles, numberOfIndices); + this.expectedEstimatedNumberOfBytes = expectedEstimatedNumberOfBytes; + } + + @Parameterized.Parameters(name = "{0} indices; {1} roles; estimated number of bytes: {2}") + public static Collection params() { + List result = new ArrayList<>(); + + // indices; roles; expected number of bytes + result.add(new Object[] { 100, 10, 10_000 }); + result.add(new Object[] { 100, 100, 13_000 }); + result.add(new Object[] { 100, 1000, 26_000 }); + + result.add(new Object[] { 1000, 10, 92_000 }); + result.add(new Object[] { 1000, 100, 94_000 }); + result.add(new Object[] { 1000, 1000, 112_000 }); + + result.add(new Object[] { 10_000, 10, 890_000 }); + result.add(new Object[] { 10_000, 100, 930_000 }); + + return result; + } + + static Map createIndices(int numberOfIndices) { + String[] names = new String[numberOfIndices]; + + for (int i = 0; i < numberOfIndices; i++) { + names[i] = "index_" + i; + } + + return MockIndexMetadataBuilder.indices(names).build().getIndicesLookup(); + } + + static SecurityDynamicConfiguration createRoles(int numberOfRoles, int numberOfIndices) { + try { + Random random = new Random(1); + Map rolesDocument = new HashMap<>(); + List allowedActions = Arrays.asList( + "indices:data/read*", + "indices:admin/mappings/fields/get*", + "indices:admin/resolve/index", + "indices:data/write*", + "indices:admin/mapping/put" + ); + + for (int i = 0; i < numberOfRoles; i++) { + List indexPatterns = new ArrayList<>(); + int numberOfIndexPatterns = Math.min( + (int) ((Math.abs(random.nextGaussian() + 0.3)) * 0.5 * numberOfIndices), + numberOfIndices + ); + + int numberOfIndexPatterns10th = numberOfIndexPatterns / 10; + + if (numberOfIndexPatterns10th > 0) { + for (int k = 0; k < numberOfIndexPatterns10th; k++) { + indexPatterns.add("index_" + random.nextInt(numberOfIndices / 10) + "*"); + } + } else { + for (int k = 0; k < numberOfIndexPatterns; k++) { + indexPatterns.add("index_" + random.nextInt(numberOfIndices)); + } + } + + Map roleDocument = ImmutableMap.of( + "index_permissions", + Arrays.asList(ImmutableMap.of("index_patterns", indexPatterns, "allowed_actions", allowedActions)) + ); + + rolesDocument.put("role_" + i, roleDocument); + } + + return SecurityDynamicConfiguration.fromMap(rolesDocument, CType.ROLES); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + } + + static PrivilegesEvaluationContext ctx(String... roles) { + User user = new User("test_user"); + user.addAttributes(ImmutableMap.of("attrs.dept_no", "a11")); + return new PrivilegesEvaluationContext( + user, + ImmutableSet.copyOf(roles), + null, + null, + null, + null, + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), + null + ); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/privileges/ClusterStateMetadataDependentPrivilegesTest.java b/src/integrationTest/java/org/opensearch/security/privileges/ClusterStateMetadataDependentPrivilegesTest.java new file mode 100644 index 0000000000..118d5358f6 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/ClusterStateMetadataDependentPrivilegesTest.java @@ -0,0 +1,117 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.security.privileges; + +import java.util.concurrent.atomic.AtomicReference; + +import org.awaitility.Awaitility; +import org.junit.Test; + +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.node.Node; +import org.opensearch.threadpool.ThreadPool; + +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +public class ClusterStateMetadataDependentPrivilegesTest { + + @Test + public void simpleUpdate() { + ThreadPool threadPool = threadPool(); + try { + ConcreteTestSubject subject = new ConcreteTestSubject(); + ClusterState clusterState = clusterState(metadata(1)); + ClusterService clusterService = Mockito.mock(ClusterService.class); + Mockito.when(clusterService.state()).thenReturn(clusterState); + + subject.updateClusterStateMetadataAsync(clusterService, threadPool); + Awaitility.await().until(() -> subject.getCurrentlyUsedMetadataVersion() == 1); + subject.shutdown(); + } finally { + threadPool.shutdown(); + } + } + + @Test + public void frequentUpdates() throws Exception { + ThreadPool threadPool = threadPool(); + try { + ConcreteTestSubject subject = new ConcreteTestSubject(); + AtomicReference clusterStateReference = new AtomicReference<>(clusterState(metadata(1))); + ClusterService clusterService = Mockito.mock(ClusterService.class); + Mockito.when(clusterService.state()).thenAnswer((Answer) invocationOnMock -> clusterStateReference.get()); + subject.updateClusterStateMetadataAsync(clusterService, threadPool); + subject.updateClusterStateMetadataAsync(clusterService, threadPool); + + for (int i = 2; i <= 100; i++) { + clusterStateReference.set(clusterState(metadata(i))); + subject.updateClusterStateMetadataAsync(clusterService, threadPool); + Thread.sleep(10); + } + + Awaitility.await().until(() -> subject.getCurrentlyUsedMetadataVersion() == 100); + subject.shutdown(); + } finally { + threadPool.shutdown(); + } + } + + @Test + public void shutdown() { + ThreadPool threadPool = threadPool(); + try { + ConcreteTestSubject subject = new ConcreteTestSubject(); + ClusterState clusterState = clusterState(metadata(1)); + ClusterService clusterService = Mockito.mock(ClusterService.class); + Mockito.when(clusterService.state()).thenReturn(clusterState); + subject.updateClusterStateMetadataAsync(clusterService, threadPool); + subject.shutdown(); + } finally { + threadPool.shutdown(); + } + } + + static Metadata metadata(long version) { + return Metadata.builder().version(version).build(); + } + + static ClusterState clusterState(Metadata metadata) { + return ClusterState.builder(ClusterState.EMPTY_STATE).metadata(metadata).build(); + } + + static ThreadPool threadPool() { + return new ThreadPool(Settings.builder().put(Node.NODE_NAME_SETTING.getKey(), "name").build()); + } + + static class ConcreteTestSubject extends ClusterStateMetadataDependentPrivileges { + + private long currentMetadataVersion; + + @Override + protected void updateClusterStateMetadata(Metadata metadata) { + // We need to be slow with updates to test the debounce-functionality + try { + Thread.sleep(100); + } catch (InterruptedException e) {} + + this.currentMetadataVersion = metadata.version(); + } + + @Override + protected long getCurrentlyUsedMetadataVersion() { + return this.currentMetadataVersion; + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java b/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java new file mode 100644 index 0000000000..e098a605e5 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/IndexPatternTest.java @@ -0,0 +1,252 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.security.privileges; + +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.junit.Test; + +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.security.user.User; + +import static org.opensearch.security.util.MockIndexMetadataBuilder.indices; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +public class IndexPatternTest { + final static int CURRENT_YEAR = ZonedDateTime.now().get(ChronoField.YEAR); + final static int NEXT_YEAR = CURRENT_YEAR + 1; + + final static Metadata INDEX_METADATA = // + indices("index_a11", "index_a12", "index_a21", "index_a22", "index_b1", "index_b2")// + .alias("alias_a") + .of("index_a11", "index_a12", "index_a21", "index_a22")// + .alias("alias_b") + .of("index_b1", "index_b2")// + .dataStream("data_stream_a1")// + .dataStream("data_stream_b1")// + .index("index_year_" + CURRENT_YEAR)// + .index("index_year_" + NEXT_YEAR)// + .alias("alias_year_" + CURRENT_YEAR) + .of("index_current_year")// + .alias("alias_year_" + NEXT_YEAR) + .of("index_next_year")// + .build(); + final static ClusterState CLUSTER_STATE = ClusterState.builder(ClusterState.EMPTY_STATE).metadata(INDEX_METADATA).build(); + + @Test + public void constantIndex() throws Exception { + IndexPattern indexPattern = IndexPattern.from("index_a11"); + assertTrue(indexPattern.hasStaticPattern()); + assertFalse(indexPattern.hasDynamicPattern()); + assertFalse(indexPattern.isEmpty()); + assertTrue(indexPattern.dynamicOnly().isEmpty()); + assertEquals("index_a11", indexPattern.toString()); + + assertTrue(indexPattern.matches("index_a11", ctx(), INDEX_METADATA.getIndicesLookup())); + assertFalse(indexPattern.matches("index_a12", ctx(), INDEX_METADATA.getIndicesLookup())); + } + + @Test + public void constantAlias() throws Exception { + IndexPattern indexPattern = IndexPattern.from("alias_a"); + assertTrue(indexPattern.hasStaticPattern()); + assertFalse(indexPattern.hasDynamicPattern()); + + assertTrue(indexPattern.matches("alias_a", ctx(), INDEX_METADATA.getIndicesLookup())); + assertFalse(indexPattern.matches("alias_a1", ctx(), INDEX_METADATA.getIndicesLookup())); + } + + @Test + public void constantAlias_onIndex() throws Exception { + IndexPattern indexPattern = IndexPattern.from("alias_a"); + assertTrue(indexPattern.hasStaticPattern()); + assertFalse(indexPattern.hasDynamicPattern()); + + assertTrue(indexPattern.matches("index_a11", ctx(), INDEX_METADATA.getIndicesLookup())); + assertFalse(indexPattern.matches("index_b1", ctx(), INDEX_METADATA.getIndicesLookup())); + } + + @Test + public void constantDataStream_onIndex() throws Exception { + IndexPattern indexPattern = IndexPattern.from("data_stream_a1"); + assertTrue(indexPattern.hasStaticPattern()); + assertFalse(indexPattern.hasDynamicPattern()); + + assertTrue(indexPattern.matches(".ds-data_stream_a1-000001", ctx(), INDEX_METADATA.getIndicesLookup())); + assertFalse(indexPattern.matches(".ds-data_stream_a2-000001", ctx(), INDEX_METADATA.getIndicesLookup())); + } + + @Test + public void patternIndex() throws Exception { + IndexPattern indexPattern = IndexPattern.from("index_a1*"); + assertTrue(indexPattern.hasStaticPattern()); + assertFalse(indexPattern.hasDynamicPattern()); + + assertTrue(indexPattern.matches("index_a11", ctx(), INDEX_METADATA.getIndicesLookup())); + assertFalse(indexPattern.matches("index_a21", ctx(), INDEX_METADATA.getIndicesLookup())); + } + + @Test + public void patternAlias() throws Exception { + IndexPattern indexPattern = IndexPattern.from("alias_a*"); + assertTrue(indexPattern.hasStaticPattern()); + assertFalse(indexPattern.hasDynamicPattern()); + + assertTrue(indexPattern.matches("alias_a", ctx(), INDEX_METADATA.getIndicesLookup())); + assertFalse(indexPattern.matches("alias_b", ctx(), INDEX_METADATA.getIndicesLookup())); + } + + @Test + public void patternAlias_onIndex() throws Exception { + IndexPattern indexPattern = IndexPattern.from("alias_a*"); + assertTrue(indexPattern.hasStaticPattern()); + assertFalse(indexPattern.hasDynamicPattern()); + + assertTrue(indexPattern.matches("index_a11", ctx(), INDEX_METADATA.getIndicesLookup())); + assertFalse(indexPattern.matches("index_b1", ctx(), INDEX_METADATA.getIndicesLookup())); + } + + @Test + public void patternDataStream_onIndex() throws Exception { + IndexPattern indexPattern = IndexPattern.from("data_stream_a*"); + assertTrue(indexPattern.hasStaticPattern()); + assertFalse(indexPattern.hasDynamicPattern()); + + assertTrue(indexPattern.matches(".ds-data_stream_a1-000001", ctx(), INDEX_METADATA.getIndicesLookup())); + assertFalse(indexPattern.matches(".ds-data_stream_b1-000001", ctx(), INDEX_METADATA.getIndicesLookup())); + } + + /** + * Static invalid regular expressions are just ignored + */ + @Test + public void regex_invalid() throws Exception { + IndexPattern indexPattern = IndexPattern.from("/index_x\\/"); + assertFalse(indexPattern.hasStaticPattern()); + assertFalse(indexPattern.hasDynamicPattern()); + assertTrue(indexPattern.isEmpty()); + } + + @Test + public void dateMathIndex() throws Exception { + IndexPattern indexPattern = IndexPattern.from(""); + assertFalse(indexPattern.hasStaticPattern()); + assertTrue(indexPattern.hasDynamicPattern()); + assertEquals("", indexPattern.toString()); + + assertTrue(indexPattern.matches("index_year_" + CURRENT_YEAR, ctx(), INDEX_METADATA.getIndicesLookup())); + assertFalse(indexPattern.matches("index_year_" + NEXT_YEAR, ctx(), INDEX_METADATA.getIndicesLookup())); + } + + @Test + public void dateMathAlias_onIndex() throws Exception { + IndexPattern indexPattern = IndexPattern.from(""); + assertFalse(indexPattern.hasStaticPattern()); + assertTrue(indexPattern.hasDynamicPattern()); + assertFalse(indexPattern.isEmpty()); + + assertTrue(indexPattern.matches("index_current_year", ctx(), INDEX_METADATA.getIndicesLookup())); + assertFalse(indexPattern.matches("index_next_year", ctx(), INDEX_METADATA.getIndicesLookup())); + } + + @Test(expected = PrivilegesEvaluationException.class) + public void dateMathIndex_invalid() throws Exception { + IndexPattern indexPattern = IndexPattern.from(""); + indexPattern.matches("index_year_" + CURRENT_YEAR, ctx(), INDEX_METADATA.getIndicesLookup()); + } + + @Test + public void templatedIndex() throws Exception { + IndexPattern indexPattern = IndexPattern.from("index_${attrs.a11}"); + assertFalse(indexPattern.hasStaticPattern()); + assertTrue(indexPattern.hasDynamicPattern()); + assertEquals(indexPattern, indexPattern.dynamicOnly()); + + assertTrue(indexPattern.matches("index_a11", ctx(), INDEX_METADATA.getIndicesLookup())); + assertFalse(indexPattern.matches("index_a12", ctx(), INDEX_METADATA.getIndicesLookup())); + } + + @Test(expected = PrivilegesEvaluationException.class) + public void templatedIndex_invalid() throws Exception { + IndexPattern indexPattern = IndexPattern.from("/index_${attrs.a11}\\/"); + assertFalse(indexPattern.hasStaticPattern()); + assertTrue(indexPattern.hasDynamicPattern()); + + indexPattern.matches("whatever", ctx(), INDEX_METADATA.getIndicesLookup()); + } + + @Test + public void mixed() throws Exception { + IndexPattern indexPattern = IndexPattern.from("index_${attrs.a11}", "index_a12"); + assertTrue(indexPattern.hasStaticPattern()); + assertTrue(indexPattern.hasDynamicPattern()); + assertFalse(indexPattern.isEmpty()); + + assertEquals(WildcardMatcher.from("index_a12"), indexPattern.getStaticPattern()); + assertEquals(IndexPattern.from("index_${attrs.a11}"), indexPattern.dynamicOnly()); + assertEquals("index_a12 index_${attrs.a11}", indexPattern.toString()); + } + + @Test + public void mixed2() throws Exception { + IndexPattern indexPattern = IndexPattern.from("", "index_a12"); + assertTrue(indexPattern.hasStaticPattern()); + assertTrue(indexPattern.hasDynamicPattern()); + assertFalse(indexPattern.isEmpty()); + + assertEquals(WildcardMatcher.from("index_a12"), indexPattern.getStaticPattern()); + assertEquals(IndexPattern.from(""), indexPattern.dynamicOnly()); + assertEquals("index_a12 ", indexPattern.toString()); + } + + @Test + public void equals() { + IndexPattern a1 = IndexPattern.from("data_stream_a*"); + IndexPattern a2 = IndexPattern.from("data_stream_a*"); + IndexPattern b = IndexPattern.from("", "data_stream_a*"); + + assertEquals(a1, a1); + assertEquals(a1, a2); + assertNotEquals(a1, b); + assertFalse(a1.equals(a1.toString())); + } + + private static PrivilegesEvaluationContext ctx() { + IndexNameExpressionResolver indexNameExpressionResolver = new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)); + IndexResolverReplacer indexResolverReplacer = new IndexResolverReplacer(indexNameExpressionResolver, () -> CLUSTER_STATE, null); + User user = new User("test_user"); + user.addAttributes(ImmutableMap.of("attrs.a11", "a11")); + user.addAttributes(ImmutableMap.of("attrs.year", "year")); + + return new PrivilegesEvaluationContext( + user, + ImmutableSet.of(), + "indices:action/test", + null, + null, + indexResolverReplacer, + indexNameExpressionResolver, + () -> CLUSTER_STATE + ); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/privileges/PrivilegeEvaluatorResponseMatcher.java b/src/integrationTest/java/org/opensearch/security/privileges/PrivilegeEvaluatorResponseMatcher.java new file mode 100644 index 0000000000..dfaa065605 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/PrivilegeEvaluatorResponseMatcher.java @@ -0,0 +1,182 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.security.privileges; + +import java.util.Arrays; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import org.hamcrest.Description; +import org.hamcrest.DiagnosingMatcher; + +/** + * Provides hamcrest matchers for PrivilegesEvaluatorResponse instances, which can be used with assertThat() calls. + */ +public abstract class PrivilegeEvaluatorResponseMatcher extends DiagnosingMatcher { + + /** + * Asserts that the status of the PrivilegesEvaluatorResponse is "allowed". + */ + public static PrivilegeEvaluatorResponseMatcher isAllowed() { + return new PrivilegeEvaluatorResponseMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("Request is fully allowed; isAllowed() returns true"); + } + + @Override + protected boolean matches(PrivilegesEvaluatorResponse response, Description mismatchDescription) { + if (!response.isAllowed()) { + mismatchDescription.appendText("isAllowed() is false"); + return false; + } + + if (response.isPartiallyOk()) { + mismatchDescription.appendText("isPartiallyOk() must be false if isAllowed() is true"); + return false; + } + + if (!response.getMissingPrivileges().isEmpty()) { + mismatchDescription.appendText("getMissingPrivileges() must be empty if isAllowed() is true"); + return false; + } + + return true; + } + }; + } + + /** + * Asserts that the status of the PrivilegesEvaluatorResponse is neither "allowed" or "partially allowed". You can + * add missingPrivileges sub-matchers to verify the actually missing privileges. + */ + public static PrivilegeEvaluatorResponseMatcher isForbidden(PrivilegeEvaluatorResponseMatcher... subMatchers) { + return new PrivilegeEvaluatorResponseMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("Request is fully forbidden; isAllowed() returns false; isPartiallyOk() returns false"); + + for (PrivilegeEvaluatorResponseMatcher subMatcher : subMatchers) { + description.appendText("; "); + subMatcher.describeTo(description); + } + } + + @Override + protected boolean matches(PrivilegesEvaluatorResponse response, Description mismatchDescription) { + if (response.isAllowed()) { + mismatchDescription.appendText("isAllowed() is true"); + return false; + } + + if (response.isPartiallyOk()) { + mismatchDescription.appendText("isPartiallyOk() is true"); + return false; + } + + for (PrivilegeEvaluatorResponseMatcher subMatcher : subMatchers) { + if (!subMatcher.matches(response, mismatchDescription)) { + return false; + } + } + + return true; + } + }; + } + + /** + * Asserts that the status of the PrivilegesEvaluatorResponse is "partially ok". You can specify the available + * indices are parameter. + */ + public static PrivilegeEvaluatorResponseMatcher isPartiallyOk(String... availableIndices) { + return new PrivilegeEvaluatorResponseMatcher() { + @Override + public void describeTo(Description description) { + description.appendText( + "Request is allowed for a subset of indices; isPartiallyOk() returns true; getAvailableIndices() returns " + ).appendValue(Arrays.asList(availableIndices)); + } + + @Override + protected boolean matches(PrivilegesEvaluatorResponse response, Description mismatchDescription) { + if (!response.isPartiallyOk()) { + mismatchDescription.appendText("isPartiallyOk() is false"); + return false; + } + + if (response.isAllowed()) { + mismatchDescription.appendText("isAllowed() must be false if isPartiallyOk() is true"); + return false; + } + + if (!response.getAvailableIndices().equals(ImmutableSet.copyOf(availableIndices))) { + mismatchDescription.appendText("getAvailableIndices() is ").appendValue(Arrays.asList(response.getAvailableIndices())); + return false; + } + + return true; + } + }; + } + + /** + * Asserts that the missingPrivileges property of a PrivilegesEvaluatorResponse instance equals to the given parameters. + * Should be used as a sub-matcher for isForbidden(). + */ + public static PrivilegeEvaluatorResponseMatcher missingPrivileges(String... missingPrivileges) { + return missingPrivileges(ImmutableSet.copyOf(missingPrivileges)); + } + + /** + * Asserts that the missingPrivileges property of a PrivilegesEvaluatorResponse instance equals to the given parameters. + * Should be used as a sub-matcher for isForbidden(). + */ + public static PrivilegeEvaluatorResponseMatcher missingPrivileges(Set missingPrivileges) { + return new PrivilegeEvaluatorResponseMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("Missing privileges are "); + description.appendValue(missingPrivileges); + } + + @Override + protected boolean matches(PrivilegesEvaluatorResponse response, Description mismatchDescription) { + if (!response.getMissingPrivileges().equals(missingPrivileges)) { + mismatchDescription.appendText("getMissingPrivileges() returns ").appendValue(response.getMissingPrivileges()); + return false; + } + + return true; + } + }; + } + + @Override + protected boolean matches(Object o, Description mismatchDescription) { + if (!(o instanceof PrivilegesEvaluatorResponse)) { + mismatchDescription.appendText("The object is not an instance of PrivilegesEvaluatorResponse: ").appendValue(o); + } + + PrivilegesEvaluatorResponse response = (PrivilegesEvaluatorResponse) o; + + if (matches(response, mismatchDescription)) { + return true; + } else { + mismatchDescription.appendText("\n"); + mismatchDescription.appendText(response.toString()); + return false; + } + } + + protected abstract boolean matches(PrivilegesEvaluatorResponse response, Description mismatchDescription); + +} diff --git a/src/integrationTest/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java b/src/integrationTest/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java index eb8c61e679..538f03ee37 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java @@ -19,6 +19,7 @@ import org.opensearch.script.mustache.MustachePlugin; import org.opensearch.script.mustache.RenderSearchTemplateAction; +import org.opensearch.test.framework.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.cluster.ClusterManager; @@ -65,11 +66,18 @@ public class PrivilegesEvaluatorTest { private String TEST_RENDER_SEARCH_TEMPLATE_QUERY = "{\"params\":{\"status\":[\"pending\",\"published\"]},\"source\":\"{\\\"query\\\": {\\\"terms\\\": {\\\"status\\\": [\\\"{{#status}}\\\",\\\"{{.}}\\\",\\\"{{/status}}\\\"]}}}\"}"; + final static TestIndex R = TestIndex.name("r").build(); + /** + * This is necessary so that the testNegativeLookaheadPattern test has an forbidden index to match against + */ + final static TestIndex T = TestIndex.name("t").build(); + @ClassRule public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) .authc(AUTHC_HTTPBASIC_INTERNAL) .users(NEGATIVE_LOOKAHEAD, NEGATED_REGEX, SEARCH_TEMPLATE, RENDER_SEARCH_TEMPLATE, TestSecurityConfig.User.USER_ADMIN) .plugin(MustachePlugin.class) + .indices(R, T) .build(); @Test diff --git a/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java new file mode 100644 index 0000000000..1e61aa0206 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/RestEndpointPermissionTests.java @@ -0,0 +1,257 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.privileges; + +import java.io.IOException; +import java.util.AbstractMap.SimpleEntry; +import java.util.Arrays; +import java.util.Collection; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Assert; +import org.junit.Test; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.dlic.rest.api.Endpoint; +import org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.PermissionBuilder; +import org.opensearch.security.securityconf.FlattenedActionGroups; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.RoleV7; +import org.opensearch.security.user.User; + +import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION; +import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS; +import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.RELOAD_CERTS_ACTION; +import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE; + +/** + * Moved from https://github.com/opensearch-project/security/blob/54361468f5c4b3a57f3ecffaf1bbe8dccee562be/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsTest.java + * + * See https://github.com/opensearch-project/security/pull/2411 + */ +public class RestEndpointPermissionTests { + + static final Map NO_REST_ADMIN_PERMISSIONS_ROLES = ImmutableMap.builder() + .put("all_access", role("*")) + .put("all_cluster_and_indices", role("custer:*", "indices:*")) + .build(); + + static final Map REST_ADMIN_PERMISSIONS_FULL_ACCESS_ROLES = ImmutableMap.builder() + .put("security_rest_api_full_access", role(allRestApiPermissions())) + .put("security_rest_api_full_access_with_star", role("restapi:admin/*")) + .build(); + + static String restAdminApiRoleName(final String endpoint) { + return String.format("security_rest_api_%s_only", endpoint); + } + + static final Map REST_ADMIN_PERMISSIONS_ROLES = ENDPOINTS_WITH_PERMISSIONS.entrySet().stream().flatMap(e -> { + final String endpoint = e.getKey().name().toLowerCase(Locale.ROOT); + final PermissionBuilder pb = e.getValue(); + if (e.getKey() == Endpoint.SSL) { + return Stream.of( + new SimpleEntry<>(restAdminApiRoleName(CERTS_INFO_ACTION), role(pb.build(CERTS_INFO_ACTION))), + new SimpleEntry<>(restAdminApiRoleName(RELOAD_CERTS_ACTION), role(pb.build(RELOAD_CERTS_ACTION))) + ); + } else if (e.getKey() == Endpoint.CONFIG) { + return Stream.of(new SimpleEntry<>(restAdminApiRoleName(SECURITY_CONFIG_UPDATE), role(pb.build(SECURITY_CONFIG_UPDATE)))); + } else { + return Stream.of(new SimpleEntry<>(restAdminApiRoleName(endpoint), role(pb.build()))); + } + }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + static ObjectNode role(final String... clusterPermissions) { + final ArrayNode clusterPermissionsArrayNode = DefaultObjectMapper.objectMapper.createArrayNode(); + Arrays.stream(clusterPermissions).forEach(clusterPermissionsArrayNode::add); + return DefaultObjectMapper.objectMapper.createObjectNode() + .put("reserved", true) + .set("cluster_permissions", clusterPermissionsArrayNode); + } + + static String[] allRestApiPermissions() { + return ENDPOINTS_WITH_PERMISSIONS.entrySet().stream().flatMap(entry -> { + if (entry.getKey() == Endpoint.SSL) { + return Stream.of(entry.getValue().build(CERTS_INFO_ACTION), entry.getValue().build(RELOAD_CERTS_ACTION)); + } else if (entry.getKey() == Endpoint.CONFIG) { + return Stream.of(entry.getValue().build(SECURITY_CONFIG_UPDATE)); + } else { + return Stream.of(entry.getValue().build()); + } + }).toArray(String[]::new); + } + + final ActionPrivileges actionPrivileges; + + public RestEndpointPermissionTests() throws IOException { + this.actionPrivileges = new ActionPrivileges(createRolesConfig(), FlattenedActionGroups.EMPTY, null, Settings.EMPTY); + } + + @Test + public void hasNoExplicitClusterPermissionPermissionForRestAdmin() { + for (final String role : NO_REST_ADMIN_PERMISSIONS_ROLES.keySet()) { + for (final Map.Entry entry : ENDPOINTS_WITH_PERMISSIONS.entrySet()) { + final Endpoint endpoint = entry.getKey(); + final PermissionBuilder permissionBuilder = entry.getValue(); + if (endpoint == Endpoint.SSL) { + Assert.assertFalse( + endpoint.name(), + actionPrivileges.hasExplicitClusterPrivilege(ctx(role), permissionBuilder.build(CERTS_INFO_ACTION)).isAllowed() + ); + Assert.assertFalse( + endpoint.name(), + actionPrivileges.hasExplicitClusterPrivilege(ctx(role), permissionBuilder.build(RELOAD_CERTS_ACTION)).isAllowed() + ); + } else if (endpoint == Endpoint.CONFIG) { + Assert.assertFalse( + endpoint.name(), + actionPrivileges.hasExplicitClusterPrivilege(ctx(role), permissionBuilder.build(SECURITY_CONFIG_UPDATE)).isAllowed() + ); + } else { + Assert.assertFalse( + endpoint.name(), + actionPrivileges.hasExplicitClusterPrivilege(ctx(role), permissionBuilder.build()).isAllowed() + ); + } + } + } + } + + @Test + public void hasExplicitClusterPermissionPermissionForRestAdminWitFullAccess() { + for (final String role : REST_ADMIN_PERMISSIONS_FULL_ACCESS_ROLES.keySet()) { + for (final Map.Entry entry : ENDPOINTS_WITH_PERMISSIONS.entrySet()) { + final Endpoint endpoint = entry.getKey(); + final PermissionBuilder permissionBuilder = entry.getValue(); + if (endpoint == Endpoint.SSL) { + Assert.assertTrue( + endpoint.name() + "/" + CERTS_INFO_ACTION, + actionPrivileges.hasExplicitClusterPrivilege(ctx(role), permissionBuilder.build(CERTS_INFO_ACTION)).isAllowed() + ); + Assert.assertTrue( + endpoint.name() + "/" + CERTS_INFO_ACTION, + actionPrivileges.hasExplicitClusterPrivilege(ctx(role), permissionBuilder.build(RELOAD_CERTS_ACTION)).isAllowed() + ); + } else if (endpoint == Endpoint.CONFIG) { + Assert.assertTrue( + endpoint.name() + "/" + SECURITY_CONFIG_UPDATE, + actionPrivileges.hasExplicitClusterPrivilege(ctx(role), permissionBuilder.build(SECURITY_CONFIG_UPDATE)).isAllowed() + ); + } else { + Assert.assertTrue( + endpoint.name(), + actionPrivileges.hasExplicitClusterPrivilege(ctx(role), permissionBuilder.build()).isAllowed() + ); + } + } + } + } + + @Test + public void hasExplicitClusterPermissionPermissionForRestAdmin() { + // verify all endpoint except SSL and verify CONFIG endpoints + final Collection noSslEndpoints = ENDPOINTS_WITH_PERMISSIONS.keySet() + .stream() + .filter(e -> e != Endpoint.SSL && e != Endpoint.CONFIG) + .collect(Collectors.toList()); + for (final Endpoint endpoint : noSslEndpoints) { + final String permission = ENDPOINTS_WITH_PERMISSIONS.get(endpoint).build(); + final PrivilegesEvaluationContext ctx = ctx(restAdminApiRoleName(endpoint.name().toLowerCase(Locale.ROOT))); + Assert.assertTrue(endpoint.name(), actionPrivileges.hasExplicitClusterPrivilege(ctx, permission).isAllowed()); + assertHasNoPermissionsForRestApiAdminOnePermissionRole(endpoint, ctx); + } + // verify SSL endpoint with 2 actions + for (final String sslAction : ImmutableSet.of(CERTS_INFO_ACTION, RELOAD_CERTS_ACTION)) { + final PrivilegesEvaluationContext ctx = ctx(restAdminApiRoleName(sslAction)); + final PermissionBuilder permissionBuilder = ENDPOINTS_WITH_PERMISSIONS.get(Endpoint.SSL); + Assert.assertTrue( + Endpoint.SSL + "/" + sslAction, + actionPrivileges.hasExplicitClusterPrivilege(ctx, permissionBuilder.build(sslAction)).isAllowed() + ); + assertHasNoPermissionsForRestApiAdminOnePermissionRole(Endpoint.SSL, ctx); + } + // verify CONFIG endpoint with 1 action + final PrivilegesEvaluationContext ctx = ctx(restAdminApiRoleName(SECURITY_CONFIG_UPDATE)); + final PermissionBuilder permissionBuilder = ENDPOINTS_WITH_PERMISSIONS.get(Endpoint.CONFIG); + Assert.assertTrue( + Endpoint.SSL + "/" + SECURITY_CONFIG_UPDATE, + actionPrivileges.hasExplicitClusterPrivilege(ctx, permissionBuilder.build(SECURITY_CONFIG_UPDATE)).isAllowed() + ); + assertHasNoPermissionsForRestApiAdminOnePermissionRole(Endpoint.CONFIG, ctx); + } + + void assertHasNoPermissionsForRestApiAdminOnePermissionRole(final Endpoint allowEndpoint, final PrivilegesEvaluationContext ctx) { + final Collection noPermissionEndpoints = ENDPOINTS_WITH_PERMISSIONS.keySet() + .stream() + .filter(e -> e != allowEndpoint) + .collect(Collectors.toList()); + for (final Endpoint endpoint : noPermissionEndpoints) { + final PermissionBuilder permissionBuilder = ENDPOINTS_WITH_PERMISSIONS.get(endpoint); + if (endpoint == Endpoint.SSL) { + Assert.assertFalse( + endpoint.name(), + actionPrivileges.hasExplicitClusterPrivilege(ctx, permissionBuilder.build(CERTS_INFO_ACTION)).isAllowed() + ); + Assert.assertFalse( + endpoint.name(), + actionPrivileges.hasExplicitClusterPrivilege(ctx, permissionBuilder.build(RELOAD_CERTS_ACTION)).isAllowed() + ); + } else { + Assert.assertFalse( + endpoint.name(), + actionPrivileges.hasExplicitClusterPrivilege(ctx, permissionBuilder.build()).isAllowed() + ); + } + } + } + + static ObjectNode meta(final String type) { + return DefaultObjectMapper.objectMapper.createObjectNode().put("type", type).put("config_version", 2); + } + + static SecurityDynamicConfiguration createRolesConfig() throws IOException { + final ObjectNode rolesNode = DefaultObjectMapper.objectMapper.createObjectNode(); + rolesNode.set("_meta", meta("roles")); + NO_REST_ADMIN_PERMISSIONS_ROLES.forEach(rolesNode::set); + REST_ADMIN_PERMISSIONS_FULL_ACCESS_ROLES.forEach(rolesNode::set); + REST_ADMIN_PERMISSIONS_ROLES.forEach(rolesNode::set); + return SecurityDynamicConfiguration.fromNode(rolesNode, CType.ROLES, 2, 0, 0); + } + + static PrivilegesEvaluationContext ctx(String... roles) { + return new PrivilegesEvaluationContext(new User("test_user"), ImmutableSet.copyOf(roles), null, null, null, null, null, null); + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/privileges/legacy/PrivilegesEvaluatorTest.java b/src/integrationTest/java/org/opensearch/security/privileges/legacy/PrivilegesEvaluatorTest.java new file mode 100644 index 0000000000..9f0004c1a4 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/legacy/PrivilegesEvaluatorTest.java @@ -0,0 +1,155 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.privileges.legacy; + +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.HttpStatus; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.script.mustache.MustachePlugin; +import org.opensearch.script.mustache.RenderSearchTemplateAction; +import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class PrivilegesEvaluatorTest { + + protected final static TestSecurityConfig.User NEGATIVE_LOOKAHEAD = new TestSecurityConfig.User("negative_lookahead_user").roles( + new Role("negative_lookahead_role").indexPermissions("read").on("/^(?!t.*).*/").clusterPermissions("cluster_composite_ops") + ); + + protected final static TestSecurityConfig.User NEGATED_REGEX = new TestSecurityConfig.User("negated_regex_user").roles( + new Role("negated_regex_role").indexPermissions("read").on("/^[a-z].*/").clusterPermissions("cluster_composite_ops") + ); + + protected final static TestSecurityConfig.User SEARCH_TEMPLATE = new TestSecurityConfig.User("search_template_user").roles( + new Role("search_template_role").indexPermissions("read").on("services").clusterPermissions("cluster_composite_ops") + ); + + protected final static TestSecurityConfig.User RENDER_SEARCH_TEMPLATE = new TestSecurityConfig.User("render_search_template_user") + .roles( + new Role("render_search_template_role").indexPermissions("read") + .on("services") + .clusterPermissions(RenderSearchTemplateAction.NAME) + ); + + private String TEST_QUERY = + "{\"source\":{\"query\":{\"match\":{\"service\":\"{{service_name}}\"}}},\"params\":{\"service_name\":\"Oracle\"}}"; + + private String TEST_DOC = "{\"source\": {\"title\": \"Spirited Away\"}}"; + + private String TEST_RENDER_SEARCH_TEMPLATE_QUERY = + "{\"params\":{\"status\":[\"pending\",\"published\"]},\"source\":\"{\\\"query\\\": {\\\"terms\\\": {\\\"status\\\": [\\\"{{#status}}\\\",\\\"{{.}}\\\",\\\"{{/status}}\\\"]}}}\"}"; + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(NEGATIVE_LOOKAHEAD, NEGATED_REGEX, SEARCH_TEMPLATE, RENDER_SEARCH_TEMPLATE, TestSecurityConfig.User.USER_ADMIN) + .plugin(MustachePlugin.class) + .nodeSettings(Map.of(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), true)) + .build(); + + @Test + public void testNegativeLookaheadPattern() throws Exception { + + try (TestRestClient client = cluster.getRestClient(NEGATIVE_LOOKAHEAD)) { + assertThat(client.get("*/_search").getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + assertThat(client.get("r*/_search").getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + } + + @Test + public void testRegexPattern() throws Exception { + + try (TestRestClient client = cluster.getRestClient(NEGATED_REGEX)) { + assertThat(client.get("*/_search").getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + assertThat(client.get("r*/_search").getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + + } + + @Test + public void testSearchTemplateRequestSuccess() { + // Insert doc into services index with admin user + try (TestRestClient client = cluster.getRestClient(TestSecurityConfig.User.USER_ADMIN)) { + TestRestClient.HttpResponse response = client.postJson("services/_doc", TEST_DOC); + assertThat(response.getStatusCode(), equalTo(HttpStatus.SC_CREATED)); + } + + try (TestRestClient client = cluster.getRestClient(SEARCH_TEMPLATE)) { + final String searchTemplateOnServicesIndex = "services/_search/template"; + final TestRestClient.HttpResponse searchTemplateOnAuthorizedIndexResponse = client.getWithJsonBody( + searchTemplateOnServicesIndex, + TEST_QUERY + ); + assertThat(searchTemplateOnAuthorizedIndexResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + } + + @Test + public void testSearchTemplateRequestUnauthorizedIndex() { + try (TestRestClient client = cluster.getRestClient(SEARCH_TEMPLATE)) { + final String searchTemplateOnMoviesIndex = "movies/_search/template"; + final TestRestClient.HttpResponse searchTemplateOnUnauthorizedIndexResponse = client.getWithJsonBody( + searchTemplateOnMoviesIndex, + TEST_QUERY + ); + assertThat(searchTemplateOnUnauthorizedIndexResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + } + } + + @Test + public void testSearchTemplateRequestUnauthorizedAllIndices() { + try (TestRestClient client = cluster.getRestClient(SEARCH_TEMPLATE)) { + final String searchTemplateOnAllIndices = "_search/template"; + final TestRestClient.HttpResponse searchOnAllIndicesResponse = client.getWithJsonBody(searchTemplateOnAllIndices, TEST_QUERY); + assertThat(searchOnAllIndicesResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + } + } + + @Test + public void testRenderSearchTemplateRequestFailure() { + try (TestRestClient client = cluster.getRestClient(SEARCH_TEMPLATE)) { + final String renderSearchTemplate = "_render/template"; + final TestRestClient.HttpResponse renderSearchTemplateResponse = client.postJson( + renderSearchTemplate, + TEST_RENDER_SEARCH_TEMPLATE_QUERY + ); + assertThat(renderSearchTemplateResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + } + } + + @Test + public void testRenderSearchTemplateRequestSuccess() { + try (TestRestClient client = cluster.getRestClient(RENDER_SEARCH_TEMPLATE)) { + final String renderSearchTemplate = "_render/template"; + final TestRestClient.HttpResponse renderSearchTemplateResponse = client.postJson( + renderSearchTemplate, + TEST_RENDER_SEARCH_TEMPLATE_QUERY + ); + assertThat(renderSearchTemplateResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java b/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java index c41b5f4cda..ade540f85c 100644 --- a/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java +++ b/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java @@ -77,7 +77,7 @@ public class WhoAmITests { protected final String expectedAuthorizedBody = "{\"dn\":null,\"is_admin\":false,\"is_node_certificate_request\":false}"; protected final String expectedUnuauthorizedBody = - "no permissions for [security:whoamiprotected] and User [name=who_am_i_user_no_perm, backend_roles=[], requestedTenant=null]"; + "no permissions for [any of [cluster:admin/opendistro_security/whoamiprotected, security:whoamiprotected]] and User [name=who_am_i_user_no_perm, backend_roles=[], requestedTenant=null]"; public static final String WHOAMI_ENDPOINT = "_plugins/_security/whoami"; public static final String WHOAMI_PROTECTED_ENDPOINT = "_plugins/_security/whoamiprotected"; diff --git a/src/integrationTest/java/org/opensearch/security/util/MockIndexMetadataBuilder.java b/src/integrationTest/java/org/opensearch/security/util/MockIndexMetadataBuilder.java new file mode 100644 index 0000000000..cb0e4f32c4 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/util/MockIndexMetadataBuilder.java @@ -0,0 +1,134 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.security.util; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.opensearch.Version; +import org.opensearch.cluster.metadata.AliasMetadata; +import org.opensearch.cluster.metadata.DataStream; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.index.Index; + +/** + * Creates mocks of org.opensearch.cluster.metadata.IndexAbstraction maps. Useful for unit testing code which + * operates on index metadata. + */ +public class MockIndexMetadataBuilder { + + private static final Settings INDEX_SETTINGS = Settings.builder() + .put(IndexMetadata.SETTING_INDEX_VERSION_CREATED.getKey(), Version.CURRENT) + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 1) + .build(); + + private Metadata.Builder delegate = new Metadata.Builder(); + private Map nameToIndexMetadataBuilderMap = new HashMap<>(); + + public static MockIndexMetadataBuilder indices(String... indices) { + MockIndexMetadataBuilder builder = new MockIndexMetadataBuilder(); + + for (String index : indices) { + builder.index(index); + } + + return builder; + } + + public static MockIndexMetadataBuilder dataStreams(String... dataStreams) { + MockIndexMetadataBuilder builder = new MockIndexMetadataBuilder(); + + for (String dataStream : dataStreams) { + builder.dataStream(dataStream); + } + + return builder; + } + + public Metadata build() { + for (IndexMetadata.Builder indexMetadataBuilder : nameToIndexMetadataBuilderMap.values()) { + this.delegate.put(indexMetadataBuilder); + } + + return this.delegate.build(); + } + + public MockIndexMetadataBuilder index(String indexName) { + return index(indexName, IndexMetadata.State.OPEN); + } + + public MockIndexMetadataBuilder index(String indexName, IndexMetadata.State state) { + getIndexMetadataBuilder(indexName, state); + return this; + } + + public AliasBuilder alias(String alias) { + return new AliasBuilder(alias); + } + + public MockIndexMetadataBuilder dataStream(String dataStream) { + return dataStream(dataStream, 3); + } + + public MockIndexMetadataBuilder dataStream(String dataStream, int generations) { + List backingIndices = new ArrayList<>(); + + for (int i = 1; i <= generations; i++) { + String backingIndexName = DataStream.getDefaultBackingIndexName(dataStream, i); + backingIndices.add(new Index(backingIndexName, backingIndexName)); + getIndexMetadataBuilder(backingIndexName, IndexMetadata.State.OPEN); + } + + DataStream dataStreamMetadata = new DataStream(dataStream, new DataStream.TimestampField("@timestamp"), backingIndices); + this.delegate.put(dataStreamMetadata); + + return this; + } + + private IndexMetadata.Builder getIndexMetadataBuilder(String indexName, IndexMetadata.State state) { + IndexMetadata.Builder result = this.nameToIndexMetadataBuilderMap.get(indexName); + + if (result != null) { + return result; + } + + result = new IndexMetadata.Builder(indexName).state(state) + .settings(Settings.builder().put(INDEX_SETTINGS).put(IndexMetadata.SETTING_INDEX_UUID, indexName).build()); + + this.nameToIndexMetadataBuilderMap.put(indexName, result); + + return result; + } + + public class AliasBuilder { + private String aliasName; + + private AliasBuilder(String alias) { + this.aliasName = alias; + } + + public MockIndexMetadataBuilder of(String... indices) { + AliasMetadata aliasMetadata = new AliasMetadata.Builder(aliasName).build(); + + for (String index : indices) { + IndexMetadata.Builder indexMetadataBuilder = getIndexMetadataBuilder(index, IndexMetadata.State.OPEN); + indexMetadataBuilder.putAlias(aliasMetadata); + } + + return MockIndexMetadataBuilder.this; + } + } +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 9edf77f75c..fe23d66639 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -62,6 +62,12 @@ import org.opensearch.security.hasher.PasswordHasher; import org.opensearch.security.hasher.PasswordHasherFactory; import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.ActionGroupsV7; +import org.opensearch.security.securityconf.impl.v7.ConfigV7; +import org.opensearch.security.securityconf.impl.v7.InternalUserV7; +import org.opensearch.security.securityconf.impl.v7.RoleMappingsV7; +import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.support.ConfigConstants; import org.opensearch.test.framework.cluster.OpenSearchClientProvider.UserCredentialsHolder; @@ -160,6 +166,13 @@ public TestSecurityConfig user(User user) { return this; } + public TestSecurityConfig users(User... users) { + for (User user : users) { + this.user(user); + } + return this; + } + public TestSecurityConfig withRestAdminUser(final String name, final String... permissions) { if (!internalUsers.containsKey(name)) { user(new User(name, "REST Admin with permissions: " + Arrays.toString(permissions)).reserved(true)); @@ -517,6 +530,10 @@ public Object getAttribute(String attributeName) { return attributes.get(attributeName); } + public Map getAttributes() { + return this.attributes; + } + @Override public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { xContentBuilder.startObject(); @@ -1023,6 +1040,52 @@ public void updateInternalUsersConfiguration(Client client, List users) { updateConfigInIndex(client, CType.INTERNALUSERS, userMap); } + public SecurityDynamicConfiguration getSecurityConfiguration() { + try { + return SecurityDynamicConfiguration.fromJson( + singleEntryConfigToJson(CType.CONFIG, CType.CONFIG.toLCString(), config), + CType.CONFIG, + 2, + 0, + 0 + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public SecurityDynamicConfiguration getInternalUserConfiguration() { + try { + return SecurityDynamicConfiguration.fromJson(configToJson(CType.INTERNALUSERS, internalUsers), CType.INTERNALUSERS, 2, 0, 0); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public SecurityDynamicConfiguration getRolesConfiguration() { + try { + return SecurityDynamicConfiguration.fromJson(configToJson(CType.ROLES, roles), CType.ROLES, 2, 0, 0); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public SecurityDynamicConfiguration getRoleMappingsConfiguration() { + try { + return SecurityDynamicConfiguration.fromJson(configToJson(CType.ROLESMAPPING, rolesMapping), CType.ROLESMAPPING, 2, 0, 0); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public SecurityDynamicConfiguration geActionGroupsConfiguration() { + try { + return SecurityDynamicConfiguration.fromJson(configToJson(CType.ACTIONGROUPS, actionGroups), CType.ACTIONGROUPS, 2, 0, 0); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + static String hashPassword(final String clearTextPassword) { return passwordHasher.hash(clearTextPassword.toCharArray()); } @@ -1095,25 +1158,30 @@ private static String configToJson(CType configType, Map configType, String configurationRoot, ToXContentObject config) + throws IOException { + XContentBuilder builder = XContentFactory.jsonBuilder(); + + builder.startObject(); + builder.startObject("_meta"); + builder.field("type", configType.toLCString()); + builder.field("config_version", 2); + builder.endObject(); + + builder.field(configurationRoot, config); + + builder.endObject(); + + return builder.toString(); + } + private void writeSingleEntryConfigToIndex(Client client, CType configType, ToXContentObject config) { writeSingleEntryConfigToIndex(client, configType, configType.toLCString(), config); } private void writeSingleEntryConfigToIndex(Client client, CType configType, String configurationRoot, ToXContentObject config) { try { - XContentBuilder builder = XContentFactory.jsonBuilder(); - - builder.startObject(); - builder.startObject("_meta"); - builder.field("type", configType.toLCString()); - builder.field("config_version", 2); - builder.endObject(); - - builder.field(configurationRoot, config); - - builder.endObject(); - - String json = builder.toString(); + String json = singleEntryConfigToJson(configType, configurationRoot, config); log.info("Writing security plugin configuration into index " + configType + ":\n" + json); diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index d2c53c1de7..5ae8c0b125 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -31,6 +31,7 @@ import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -446,6 +447,14 @@ public Builder indices(TestIndex... indices) { return this; } + /** + * Specifies test indices that shall be created upon startup of the cluster. + */ + public Builder indices(Collection indices) { + this.testIndices.addAll(indices); + return this; + } + public Builder users(TestSecurityConfig.User... users) { for (TestSecurityConfig.User user : users) { testSecurityConfig.user(user); diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index a7529ae06c..8db76eabfd 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -166,6 +166,7 @@ import org.opensearch.security.http.XFFResolver; import org.opensearch.security.identity.NoopPluginSubject; import org.opensearch.security.identity.SecurityTokenManager; +import org.opensearch.security.privileges.ActionPrivileges; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.PrivilegesInterceptor; import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; @@ -1070,7 +1071,7 @@ public Collection createComponents( this.salt = Salt.from(settings); final IndexNameExpressionResolver resolver = new IndexNameExpressionResolver(threadPool.getThreadContext()); - irr = new IndexResolverReplacer(resolver, clusterService, cih); + irr = new IndexResolverReplacer(resolver, clusterService::state, cih); final String DEFAULT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS = DefaultInterClusterRequestEvaluator.class.getName(); InterClusterRequestEvaluator interClusterRequestEvaluator = new DefaultInterClusterRequestEvaluator(settings); @@ -1120,18 +1121,43 @@ public Collection createComponents( final CompatConfig compatConfig = new CompatConfig(environment, transportPassiveAuthSetting); - evaluator = new PrivilegesEvaluator( - clusterService, - threadPool, - cr, - resolver, - auditLog, - settings, - privilegesInterceptor, - cih, - irr, - namedXContentRegistry.get() - ); + if (PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.get(settings)) { + // Use legacy implementation (non-default) + evaluator = new org.opensearch.security.privileges.legacy.PrivilegesEvaluatorImpl( + clusterService, + threadPool, + cr, + resolver, + auditLog, + settings, + privilegesInterceptor, + cih, + irr, + namedXContentRegistry.get() + ); + + restLayerEvaluator = new org.opensearch.security.privileges.legacy.RestLayerPrivilegesEvaluatorImpl(clusterService, threadPool); + } else { + // Use new implementation (default) + evaluator = new org.opensearch.security.privileges.PrivilegesEvaluatorImpl( + clusterService, + clusterService::state, + threadPool, + threadPool.getThreadContext(), + cr, + resolver, + auditLog, + settings, + privilegesInterceptor, + cih, + irr, + namedXContentRegistry.get() + ); + + restLayerEvaluator = new org.opensearch.security.privileges.RestLayerPrivilegesEvaluatorImpl( + (org.opensearch.security.privileges.PrivilegesEvaluatorImpl) evaluator + ); + } sf = new SecurityFilter(settings, evaluator, adminDns, dlsFlsValve, auditLog, threadPool, cs, compatConfig, irr, xffResolver); @@ -1143,8 +1169,6 @@ public Collection createComponents( principalExtractor = ReflectionHelper.instantiatePrincipalExtractor(principalExtractorClass); } - restLayerEvaluator = new RestLayerPrivilegesEvaluator(clusterService, threadPool); - securityRestHandler = new SecurityRestFilter( backendRegistry, restLayerEvaluator, @@ -1162,7 +1186,6 @@ public Collection createComponents( dcf.registerDCFListener(irr); dcf.registerDCFListener(xffResolver); dcf.registerDCFListener(evaluator); - dcf.registerDCFListener(restLayerEvaluator); dcf.registerDCFListener(securityRestHandler); dcf.registerDCFListener(tokenManager); if (!(auditLog instanceof NullAuditLog)) { @@ -2057,6 +2080,10 @@ public List> getSettings() { Property.Filtered ) ); + + // Privileges evaluation + settings.add(ActionPrivileges.PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE); + settings.add(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR); } return settings; diff --git a/src/main/java/org/opensearch/security/configuration/ConfigurationChangeListener.java b/src/main/java/org/opensearch/security/configuration/ConfigurationChangeListener.java index cc410c0158..761cc989d1 100644 --- a/src/main/java/org/opensearch/security/configuration/ConfigurationChangeListener.java +++ b/src/main/java/org/opensearch/security/configuration/ConfigurationChangeListener.java @@ -29,6 +29,7 @@ /** * Callback function on change particular configuration */ +@FunctionalInterface public interface ConfigurationChangeListener { /** diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 4141a3f8f5..10855bdd08 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -144,7 +144,7 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< + request + "\nevaluatedDlsFlsConfig: " + evaluatedDlsFlsConfig - + "\nresolved: " + + "\ncontext: " + resolved + "\nmode: " + mode diff --git a/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java index 8e89b60712..a5db15f2d0 100644 --- a/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java @@ -40,9 +40,10 @@ import org.opensearch.core.index.Index; import org.opensearch.index.IndexService; import org.opensearch.indices.SystemIndexRegistry; +import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; import org.opensearch.security.securityconf.ConfigModel; -import org.opensearch.security.securityconf.SecurityRoles; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.HeaderHelper; import org.opensearch.security.support.WildcardMatcher; @@ -165,10 +166,12 @@ protected final boolean isBlockedSystemIndexRequest() { // allow request without user from plugin. return systemIndexMatcher.test(index.getName()) || matchesSystemIndexRegisteredWithCore; } - final TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); - final Set mappedRoles = evaluator.mapRoles(user, caller); - final SecurityRoles securityRoles = evaluator.getSecurityRoles(mappedRoles); - return !securityRoles.isPermittedOnSystemIndex(index.getName()); + + String permission = ConfigConstants.SYSTEM_INDEX_PERMISSION; + PrivilegesEvaluationContext context = evaluator.createContext(user, permission); + PrivilegesEvaluatorResponse result = evaluator.hasExplicitIndexPrivilege(context, Set.of(permission), index.getName()); + + return !result.isAllowed(); } return true; } diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index f0ab7bb487..3323c9e38a 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -461,7 +461,7 @@ public void onFailure(Exception e) { ? String.format( "no permissions for %s and associated roles %s", pres.getMissingPrivileges(), - pres.getResolvedSecurityRoles() + context.getMappedRoles() ) : String.format("no permissions for %s and %s", pres.getMissingPrivileges(), user); } diff --git a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java index b56f3e951d..c9d10ee2fa 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java @@ -27,12 +27,14 @@ package org.opensearch.security.filter; import java.nio.file.Path; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.regex.Pattern; import javax.net.ssl.SSLPeerUnverifiedException; +import com.google.common.collect.ImmutableSet; import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -225,17 +227,12 @@ void authorizeRequest(RestHandler original, SecurityRequestChannel request, User if (routeSupportsRestAuthorization) { PrivilegesEvaluatorResponse pres = new PrivilegesEvaluatorResponse(); NamedRoute route = ((NamedRoute) handler.get()); - // if actionNames are present evaluate those first - Set actionNames = route.actionNames(); - if (actionNames != null && !actionNames.isEmpty()) { - pres = evaluator.evaluate(user, actionNames); - } - - // now if pres.allowed is still false check for the NamedRoute name as a permission - if (!pres.isAllowed()) { - String action = route.name(); - pres = evaluator.evaluate(user, Set.of(action)); - } + // Check both route.actionNames() and route.name(). The presence of either is sufficient. + Set actionNames = ImmutableSet.builder() + .addAll(route.actionNames() != null ? route.actionNames() : Collections.emptySet()) + .add(route.name()) + .build(); + pres = evaluator.evaluate(user, route.name(), actionNames); if (log.isDebugEnabled()) { log.debug(pres.toString()); diff --git a/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java new file mode 100644 index 0000000000..87ac32d090 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/ActionPrivileges.java @@ -0,0 +1,1141 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.privileges; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.cluster.metadata.DataStream; +import org.opensearch.cluster.metadata.IndexAbstraction; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.common.settings.Setting; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.unit.ByteSizeUnit; +import org.opensearch.core.common.unit.ByteSizeValue; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.securityconf.FlattenedActionGroups; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.RoleV7; +import org.opensearch.security.support.WildcardMatcher; + +import com.selectivem.collections.CheckTable; +import com.selectivem.collections.CompactMapGroupBuilder; +import com.selectivem.collections.DeduplicatingCompactSubSetBuilder; +import com.selectivem.collections.ImmutableCompactSubSet; + +/** + * This class converts role configuration into pre-computed, optimized data structures for checking privileges. + *

+ * With the exception of the statefulIndex property, instances of this class are immutable. The life-cycle of an + * instance of this class corresponds to the life-cycle of the role and action group configuration. If the role or + * action group configuration is changed, a new instance needs to be built. + */ +public class ActionPrivileges extends ClusterStateMetadataDependentPrivileges { + + /** + * This setting controls the allowed heap size of the precomputed index privileges (in the inner class StatefulIndexPrivileges). + * If the size of the indices exceed the amount of bytes configured here, it will be truncated. Privileges evaluation will + * continue to work correctly, but it will be slower. + *

+ * This settings defaults to 10 MB. This is a generous limit. Experiments have shown that an example setup with + * 10,000 indices and 1,000 roles requires about 1 MB of heap. 100,000 indices and 100 roles require about 9 MB of heap. + * (Of course, these numbers can vary widely based on the actual role configuration). + */ + public static Setting PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE = Setting.memorySizeSetting( + "plugins.security.privileges_evaluation.precomputed_privileges.max_heap_size", + new ByteSizeValue(10, ByteSizeUnit.MB), + Setting.Property.NodeScope + ); + + private static final Logger log = LogManager.getLogger(ActionPrivileges.class); + + private final ClusterPrivileges cluster; + private final IndexPrivileges index; + private final SecurityDynamicConfiguration roles; + private final FlattenedActionGroups actionGroups; + private final ImmutableSet wellKnownClusterActions; + private final ImmutableSet wellKnownIndexActions; + private final Supplier> indexMetadataSupplier; + private final ByteSizeValue statefulIndexMaxHeapSize; + + private final AtomicReference statefulIndex = new AtomicReference<>(); + + public ActionPrivileges( + SecurityDynamicConfiguration roles, + FlattenedActionGroups actionGroups, + Supplier> indexMetadataSupplier, + Settings settings, + ImmutableSet wellKnownClusterActions, + ImmutableSet wellKnownIndexActions, + ImmutableSet explicitlyRequiredIndexActions + ) { + this.cluster = new ClusterPrivileges(roles, actionGroups, wellKnownClusterActions); + this.index = new IndexPrivileges(roles, actionGroups, wellKnownIndexActions, explicitlyRequiredIndexActions); + this.roles = roles; + this.actionGroups = actionGroups; + this.wellKnownClusterActions = wellKnownClusterActions; + this.wellKnownIndexActions = wellKnownIndexActions; + this.indexMetadataSupplier = indexMetadataSupplier; + this.statefulIndexMaxHeapSize = PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.get(settings); + } + + public ActionPrivileges( + SecurityDynamicConfiguration roles, + FlattenedActionGroups actionGroups, + Supplier> indexMetadataSupplier, + Settings settings + ) { + this( + roles, + actionGroups, + indexMetadataSupplier, + settings, + WellKnownActions.CLUSTER_ACTIONS, + WellKnownActions.INDEX_ACTIONS, + WellKnownActions.EXPLICITLY_REQUIRED_INDEX_ACTIONS + ); + } + + public PrivilegesEvaluatorResponse hasClusterPrivilege(PrivilegesEvaluationContext context, String action) { + return cluster.providesPrivilege(context, action, context.getMappedRoles()); + } + + public PrivilegesEvaluatorResponse hasAnyClusterPrivilege(PrivilegesEvaluationContext context, Set actions) { + return cluster.providesAnyPrivilege(context, actions, context.getMappedRoles()); + } + + /** + * Checks whether this instance provides explicit privileges for the combination of the provided action and the + * provided roles. + *

+ * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns + * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 + *

+ * Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. + */ + public PrivilegesEvaluatorResponse hasExplicitClusterPrivilege(PrivilegesEvaluationContext context, String action) { + return cluster.providesExplicitPrivilege(context, action, context.getMappedRoles()); + } + + /** + * Checks whether this instance provides privileges for the combination of the provided action, + * the provided indices and the provided roles. + *

+ * Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + *

+ * If privileges are only available for a sub-set of indices, isPartiallyOk() will return true + * and the indices for which privileges are available are returned by getAvailableIndices(). This allows the + * do_not_fail_on_forbidden behaviour. + */ + public PrivilegesEvaluatorResponse hasIndexPrivilege( + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved resolvedIndices + ) { + PrivilegesEvaluatorResponse response = this.index.providesWildcardPrivilege(context, actions); + if (response != null) { + return response; + } + + if (!resolvedIndices.isLocalAll() && resolvedIndices.getAllIndices().isEmpty()) { + // This is necessary for requests which operate on remote indices. + // Access control for the remote indices will be performed on the remote cluster. + log.debug("No local indices; grant the request"); + return PrivilegesEvaluatorResponse.ok(); + } + + // TODO one might want to consider to create a semantic wrapper for action in order to be better tell apart + // what's the action and what's the index in the generic parameters of CheckTable. + CheckTable checkTable = CheckTable.create( + resolvedIndices.getAllIndicesResolved(context.getClusterStateSupplier(), context.getIndexNameExpressionResolver()), + actions + ); + + StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); + PrivilegesEvaluatorResponse resultFromStatefulIndex = null; + + Map indexMetadata = this.indexMetadataSupplier.get(); + + if (statefulIndex != null) { + resultFromStatefulIndex = statefulIndex.providesPrivilege(actions, resolvedIndices, context, checkTable, indexMetadata); + + if (resultFromStatefulIndex != null) { + // If we get a result from statefulIndex, we are done. + return resultFromStatefulIndex; + } + + // Otherwise, we need to carry on checking privileges using the non-stateful object. + // Note: statefulIndex.hasPermission() modifies as a side effect the checkTable. + // We can carry on using this as an intermediate result and further complete checkTable below. + } + + return this.index.providesPrivilege(context, actions, resolvedIndices, checkTable, indexMetadata); + } + + /** + * Checks whether this instance provides explicit privileges for the combination of the provided action, + * the provided indices and the provided roles. + *

+ * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns + * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 + */ + public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege( + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved resolvedIndices + ) { + CheckTable checkTable = CheckTable.create(resolvedIndices.getAllIndices(), actions); + return this.index.providesExplicitPrivilege(context, actions, resolvedIndices, checkTable, this.indexMetadataSupplier.get()); + } + + /** + * Updates the stateful index configuration with the given indices. Should be normally only called by + * updateStatefulIndexPrivilegesAsync(). Package visible for testing. + */ + void updateStatefulIndexPrivileges(Map indices, long metadataVersion) { + StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); + + indices = StatefulIndexPrivileges.relevantOnly(indices); + + if (statefulIndex == null || !statefulIndex.indices.equals(indices)) { + long start = System.currentTimeMillis(); + this.statefulIndex.set( + new StatefulIndexPrivileges(roles, actionGroups, wellKnownIndexActions, indices, metadataVersion, statefulIndexMaxHeapSize) + ); + long duration = System.currentTimeMillis() - start; + log.debug("Updating StatefulIndexPrivileges took {} ms", duration); + } else { + synchronized (this) { + // Even if the indices did not change, update the metadataVersion in statefulIndex to reflect + // that the instance is up-to-date. + if (statefulIndex.metadataVersion < metadataVersion) { + statefulIndex.metadataVersion = metadataVersion; + } + } + } + } + + @Override + protected void updateClusterStateMetadata(Metadata metadata) { + this.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), metadata.version()); + } + + @Override + protected long getCurrentlyUsedMetadataVersion() { + StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); + return statefulIndex != null ? statefulIndex.metadataVersion : 0; + } + + int getEstimatedStatefulIndexByteSize() { + StatefulIndexPrivileges statefulIndex = this.statefulIndex.get(); + + if (statefulIndex != null) { + return statefulIndex.estimatedByteSize; + } else { + return 0; + } + } + + /** + * Pre-computed, optimized cluster privilege maps. Instances of this class are immutable. + *

+ * The data structures in this class are optimized for answering the question + * "I have action A and roles [x,y,z]. Do I have authorization to execute the action?". + *

+ * The check will be possible in time O(1) for "well-known" actions when the user actually has the privileges. + */ + static class ClusterPrivileges { + + /** + * Maps names of actions to the roles that provide a privilege for the respective action. + * Note that the mapping is not comprehensive, additionally the data structures rolesWithWildcardPermissions + * and rolesToActionMatcher need to be considered for a full view of the privileges. + *

+ * This does not include privileges obtained via "*" action patterns. This is both meant as a + * optimization and to support explicit privileges. + */ + private final ImmutableMap> actionToRoles; + + /** + * This contains all role names that provide wildcard (*) privileges for cluster actions. + * This avoids a blow-up of the actionToRoles object by such roles. + */ + private final ImmutableSet rolesWithWildcardPermissions; + + /** + * This maps role names to a matcher which matches the action names this role provides privileges for. + * This is only used as a last resort if the test with actionToRole and rolesWithWildcardPermissions failed. + * This is only necessary for actions which are not contained in the list of "well-known" actions provided + * during construction. + * + * This does not include privileges obtained via "*" action patterns. This is both meant as a + * optimization and to support explicit privileges. + */ + private final ImmutableMap rolesToActionMatcher; + + private final ImmutableSet wellKnownClusterActions; + + /** + * Creates pre-computed cluster privileges based on the given parameters. + *

+ * This constructor will not throw an exception if it encounters any invalid configuration (that is, + * in particular, unparseable regular expressions). Rather, it will just log an error. This is okay, as it + * just results in fewer available privileges. However, having a proper error reporting mechanism would be + * kind of nice. + */ + ClusterPrivileges( + SecurityDynamicConfiguration roles, + FlattenedActionGroups actionGroups, + ImmutableSet wellKnownClusterActions + ) { + DeduplicatingCompactSubSetBuilder roleSetBuilder = new DeduplicatingCompactSubSetBuilder<>( + roles.getCEntries().keySet() + ); + Map> actionToRoles = new HashMap<>(); + ImmutableSet.Builder rolesWithWildcardPermissions = ImmutableSet.builder(); + ImmutableMap.Builder rolesToActionMatcher = ImmutableMap.builder(); + + for (Map.Entry entry : roles.getCEntries().entrySet()) { + try { + String roleName = entry.getKey(); + RoleV7 role = entry.getValue(); + + roleSetBuilder.next(roleName); + + ImmutableSet permissionPatterns = actionGroups.resolve(role.getCluster_permissions()); + + // This list collects all the matchers for action names that will be found for the current role + List wildcardMatchers = new ArrayList<>(); + + for (String permission : permissionPatterns) { + // If we have a permission which does not use any pattern, we just simply add it to the + // "actionToRoles" map. + // Otherwise, we match the pattern against the provided well-known cluster actions and add + // these to the "actionToRoles" map. Additionally, for the case that the well-known cluster + // actions are not complete, we also collect the matcher to be used as a last resort later. + + if (WildcardMatcher.isExact(permission)) { + actionToRoles.computeIfAbsent(permission, k -> roleSetBuilder.createSubSetBuilder()).add(roleName); + } else if (permission.equals("*")) { + // Special case: Roles with a wildcard "*" giving privileges for all actions. We will not resolve + // this stuff, but just note separately that this role just gets all the cluster privileges. + rolesWithWildcardPermissions.add(roleName); + } else { + WildcardMatcher wildcardMatcher = WildcardMatcher.from(permission); + Set matchedActions = wildcardMatcher.getMatchAny( + wellKnownClusterActions, + Collectors.toUnmodifiableSet() + ); + + for (String action : matchedActions) { + actionToRoles.computeIfAbsent(action, k -> roleSetBuilder.createSubSetBuilder()).add(roleName); + } + + wildcardMatchers.add(wildcardMatcher); + } + } + + if (!wildcardMatchers.isEmpty()) { + rolesToActionMatcher.put(roleName, WildcardMatcher.from(wildcardMatchers)); + } + } catch (Exception e) { + log.error("Unexpected exception while processing role: {}\nIgnoring role.", entry.getKey(), e); + } + } + + DeduplicatingCompactSubSetBuilder.Completed completedRoleSetBuilder = roleSetBuilder.build(); + + this.actionToRoles = actionToRoles.entrySet() + .stream() + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry -> entry.getValue().build(completedRoleSetBuilder))); + this.rolesWithWildcardPermissions = rolesWithWildcardPermissions.build(); + this.rolesToActionMatcher = rolesToActionMatcher.build(); + this.wellKnownClusterActions = wellKnownClusterActions; + } + + /** + * Checks whether this instance provides privileges for the combination of the provided action and the + * provided roles. Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. + */ + PrivilegesEvaluatorResponse providesPrivilege(PrivilegesEvaluationContext context, String action, Set roles) { + + // 1: Check roles with wildcards + if (CollectionUtils.containsAny(roles, this.rolesWithWildcardPermissions)) { + return PrivilegesEvaluatorResponse.ok(); + } + + // 2: Check well-known actions - this should cover most cases + ImmutableCompactSubSet rolesWithPrivileges = this.actionToRoles.get(action); + + if (rolesWithPrivileges != null && rolesWithPrivileges.containsAny(roles)) { + return PrivilegesEvaluatorResponse.ok(); + } + + // 3: Only if everything else fails: Check the matchers in case we have a non-well-known action + if (!this.wellKnownClusterActions.contains(action)) { + for (String role : roles) { + WildcardMatcher matcher = this.rolesToActionMatcher.get(role); + + if (matcher != null && matcher.test(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + } + + return PrivilegesEvaluatorResponse.insufficient(action); + } + + /** + * Checks whether this instance provides explicit privileges for the combination of the provided action and the + * provided roles. + *

+ * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns + * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 + *

+ * Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. + */ + PrivilegesEvaluatorResponse providesExplicitPrivilege(PrivilegesEvaluationContext context, String action, Set roles) { + + // 1: Check well-known actions - this should cover most cases + ImmutableCompactSubSet rolesWithPrivileges = this.actionToRoles.get(action); + + if (rolesWithPrivileges != null && rolesWithPrivileges.containsAny(roles)) { + return PrivilegesEvaluatorResponse.ok(); + } + + // 2: Only if everything else fails: Check the matchers in case we have a non-well-known action + if (!this.wellKnownClusterActions.contains(action)) { + for (String role : roles) { + WildcardMatcher matcher = this.rolesToActionMatcher.get(role); + + if (matcher != null && matcher.test(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + } + + return PrivilegesEvaluatorResponse.insufficient(action); + } + + /** + * Checks whether this instance provides privileges for the combination of any of the provided actions and the + * provided roles. Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + * Otherwise, allowed will be false and missingPrivileges will contain the name of the given action. + */ + PrivilegesEvaluatorResponse providesAnyPrivilege(PrivilegesEvaluationContext context, Set actions, Set roles) { + // 1: Check roles with wildcards + if (CollectionUtils.containsAny(roles, this.rolesWithWildcardPermissions)) { + return PrivilegesEvaluatorResponse.ok(); + } + + // 2: Check well-known actions - this should cover most cases + for (String action : actions) { + ImmutableCompactSubSet rolesWithPrivileges = this.actionToRoles.get(action); + + if (rolesWithPrivileges != null && rolesWithPrivileges.containsAny(roles)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + + // 3: Only if everything else fails: Check the matchers in case we have a non-well-known action + for (String action : actions) { + if (!this.wellKnownClusterActions.contains(action)) { + for (String role : roles) { + WildcardMatcher matcher = this.rolesToActionMatcher.get(role); + + if (matcher != null && matcher.test(action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + } + } + + if (actions.size() == 1) { + return PrivilegesEvaluatorResponse.insufficient(actions.iterator().next()); + } else { + return PrivilegesEvaluatorResponse.insufficient("any of " + actions); + } + } + } + + /** + * Partially pre-computed, optimized index privilege maps. Instances of this class are immutable. + *

+ * This class is independent of the actual indices present in the cluster. See StatefulIndexPermissions for a class + * that also takes actual indices into account and is thus fully pre-computed. + *

+ * Purposes of this class: + *

+ * 1. Answer the question "given an action and a set of roles, do I have wildcard index privileges" in O(1) + *

+ * 2. Pre-compute the data structures as far as possible in cases that StatefulIndexPermissions cannot check the + * permissions. This is the case when: + *

+ * a) StatefulIndexPermissions does not cover all indices + * b) The requested index does not exist (especially the case for create index actions) + * c) The index patterns use placeholders like "${user.name}" - these can be only resolved when the User object is present. + * d) The action is not among the "well known" actions. + */ + static class IndexPrivileges { + /** + * Maps role names to concrete action names to IndexPattern objects which define the indices the privileges apply to. + */ + private final ImmutableMap> rolesToActionToIndexPattern; + + /** + * Maps role names to action names matchers to IndexPattern objects which define the indices the privileges apply to. + * This is especially for "non-well-known" actions. + */ + private final ImmutableMap> rolesToActionPatternToIndexPattern; + + /** + * Maps action names to the roles which provide wildcard ("*") index privileges for the respective action. + * This allows to answer the question "given an action and a set of roles, do I have wildcard index privileges" + * in O(1) + */ + private final ImmutableMap> actionToRolesWithWildcardIndexPrivileges; + + /** + * A pre-defined set of action names that is used to pre-compute the result of action patterns. + */ + private final ImmutableSet wellKnownIndexActions; + + /** + * A pre-defined set of action names that is included in the rolesToExplicitActionToIndexPattern data structure + */ + private final ImmutableSet explicitlyRequiredIndexActions; + + /** + * Maps role names to concrete action names to IndexPattern objects which define the indices the privileges apply to. + * The action names are only explicitly granted privileges which are listed in explicitlyRequiredIndexActions. + *

+ * Compare https://github.com/opensearch-project/security/pull/2887 + */ + private final ImmutableMap> rolesToExplicitActionToIndexPattern; + + /** + * Creates pre-computed index privileges based on the given parameters. + *

+ * This constructor will not throw an exception if it encounters any invalid configuration (that is, + * in particular, unparseable regular expressions). Rather, it will just log an error. This is okay, as it + * just results in fewer available privileges. However, having a proper error reporting mechanism would be + * kind of nice. + */ + IndexPrivileges( + SecurityDynamicConfiguration roles, + FlattenedActionGroups actionGroups, + ImmutableSet wellKnownIndexActions, + ImmutableSet explicitlyRequiredIndexActions + ) { + DeduplicatingCompactSubSetBuilder roleSetBuilder = new DeduplicatingCompactSubSetBuilder<>( + roles.getCEntries().keySet() + ); + + Map> rolesToActionToIndexPattern = new HashMap<>(); + Map> rolesToActionPatternToIndexPattern = new HashMap<>(); + Map> actionToRolesWithWildcardIndexPrivileges = new HashMap<>(); + Map> rolesToExplicitActionToIndexPattern = new HashMap<>(); + + for (Map.Entry entry : roles.getCEntries().entrySet()) { + try { + String roleName = entry.getKey(); + RoleV7 role = entry.getValue(); + + roleSetBuilder.next(roleName); + + for (RoleV7.Index indexPermissions : role.getIndex_permissions()) { + ImmutableSet permissions = actionGroups.resolve(indexPermissions.getAllowed_actions()); + + for (String permission : permissions) { + // If we have a permission which does not use any pattern, we just simply add it to the + // "rolesToActionToIndexPattern" map. + // Otherwise, we match the pattern against the provided well-known index actions and add + // these to the "rolesToActionToIndexPattern" map. Additionally, for the case that the + // well-known index actions are not complete, we also collect the actionMatcher to be used + // as a last resort later. + + if (WildcardMatcher.isExact(permission)) { + rolesToActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) + .computeIfAbsent(permission, k -> new IndexPattern.Builder()) + .add(indexPermissions.getIndex_patterns()); + + if (explicitlyRequiredIndexActions.contains(permission)) { + rolesToExplicitActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) + .computeIfAbsent(permission, k -> new IndexPattern.Builder()) + .add(indexPermissions.getIndex_patterns()); + } + + if (indexPermissions.getIndex_patterns().contains("*")) { + actionToRolesWithWildcardIndexPrivileges.computeIfAbsent( + permission, + k -> roleSetBuilder.createSubSetBuilder() + ).add(roleName); + } + } else { + WildcardMatcher actionMatcher = WildcardMatcher.from(permission); + + for (String action : actionMatcher.iterateMatching(wellKnownIndexActions)) { + rolesToActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) + .computeIfAbsent(action, k -> new IndexPattern.Builder()) + .add(indexPermissions.getIndex_patterns()); + + if (indexPermissions.getIndex_patterns().contains("*")) { + actionToRolesWithWildcardIndexPrivileges.computeIfAbsent( + permission, + k -> roleSetBuilder.createSubSetBuilder() + ).add(roleName); + } + } + + rolesToActionPatternToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) + .computeIfAbsent(actionMatcher, k -> new IndexPattern.Builder()) + .add(indexPermissions.getIndex_patterns()); + + if (actionMatcher != WildcardMatcher.ANY) { + for (String action : actionMatcher.iterateMatching(explicitlyRequiredIndexActions)) { + rolesToExplicitActionToIndexPattern.computeIfAbsent(roleName, k -> new HashMap<>()) + .computeIfAbsent(action, k -> new IndexPattern.Builder()) + .add(indexPermissions.getIndex_patterns()); + } + } + } + } + } + } catch (Exception e) { + log.error("Unexpected exception while processing role: {}\nIgnoring role.", entry.getKey(), e); + } + } + + DeduplicatingCompactSubSetBuilder.Completed completedRoleSetBuilder = roleSetBuilder.build(); + + this.rolesToActionToIndexPattern = rolesToActionToIndexPattern.entrySet() + .stream() + .collect( + ImmutableMap.toImmutableMap( + Map.Entry::getKey, + entry -> entry.getValue() + .entrySet() + .stream() + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry2 -> entry2.getValue().build())) + ) + ); + + this.rolesToActionPatternToIndexPattern = rolesToActionPatternToIndexPattern.entrySet() + .stream() + .collect( + ImmutableMap.toImmutableMap( + Map.Entry::getKey, + entry -> entry.getValue() + .entrySet() + .stream() + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry2 -> entry2.getValue().build())) + ) + ); + + this.actionToRolesWithWildcardIndexPrivileges = actionToRolesWithWildcardIndexPrivileges.entrySet() + .stream() + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry -> entry.getValue().build(completedRoleSetBuilder))); + + this.rolesToExplicitActionToIndexPattern = rolesToExplicitActionToIndexPattern.entrySet() + .stream() + .collect( + ImmutableMap.toImmutableMap( + Map.Entry::getKey, + entry -> entry.getValue() + .entrySet() + .stream() + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, entry2 -> entry2.getValue().build())) + ) + ); + + this.wellKnownIndexActions = wellKnownIndexActions; + this.explicitlyRequiredIndexActions = explicitlyRequiredIndexActions; + } + + /** + * Checks whether this instance provides privileges for the combination of the provided action, + * the provided indices and the provided roles. + *

+ * Returns a PrivilegesEvaluatorResponse with allowed=true if privileges are available. + *

+ * If privileges are only available for a sub-set of indices, isPartiallyOk() will return true + * and the indices for which privileges are available are returned by getAvailableIndices(). This allows the + * do_not_fail_on_forbidden behaviour. + *

+ * This method will only verify privileges for the index/action combinations which are un-checked in + * the checkTable instance provided to this method. Checked index/action combinations are considered to be + * "already fulfilled by other means" - usually that comes from the stateful data structure. + * As a side-effect, this method will further mark the available index/action combinations in the provided + * checkTable instance as checked. + */ + PrivilegesEvaluatorResponse providesPrivilege( + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved resolvedIndices, + CheckTable checkTable, + Map indexMetadata + ) { + List exceptions = new ArrayList<>(); + + for (String role : context.getMappedRoles()) { + ImmutableMap actionToIndexPattern = this.rolesToActionToIndexPattern.get(role); + + if (actionToIndexPattern != null) { + for (String action : actions) { + IndexPattern indexPattern = actionToIndexPattern.get(action); + + if (indexPattern != null) { + for (String index : checkTable.iterateUncheckedRows(action)) { + try { + if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } catch (PrivilegesEvaluationException e) { + // We can ignore these errors, as this max leads to fewer privileges than available + log.error("Error while evaluating index pattern of role {}. Ignoring entry", role, e); + exceptions.add(new PrivilegesEvaluationException("Error while evaluating role " + role, e)); + } + } + } + } + } + } + + // If all actions are well-known, the index.rolesToActionToIndexPattern data structure that was evaluated above, + // would have contained all the actions if privileges are provided. If there are non-well-known actions among the + // actions, we also have to evaluate action patterns to check the authorization + + boolean allActionsWellKnown = actions.stream().allMatch(a -> this.wellKnownIndexActions.contains(a)); + + if (!checkTable.isComplete() && !allActionsWellKnown) { + top: for (String role : context.getMappedRoles()) { + ImmutableMap actionPatternToIndexPattern = this.rolesToActionPatternToIndexPattern.get( + role + ); + + if (actionPatternToIndexPattern != null) { + for (String action : actions) { + if (this.wellKnownIndexActions.contains(action)) { + continue; + } + + for (Map.Entry entry : actionPatternToIndexPattern.entrySet()) { + WildcardMatcher actionMatcher = entry.getKey(); + IndexPattern indexPattern = entry.getValue(); + + if (actionMatcher.test(action)) { + for (String index : checkTable.iterateUncheckedRows(action)) { + try { + if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { + break top; + } + } catch (PrivilegesEvaluationException e) { + // We can ignore these errors, as this max leads to fewer privileges than available + log.error("Error while evaluating index pattern of role {}. Ignoring entry", role, e); + exceptions.add(new PrivilegesEvaluationException("Error while evaluating role " + role, e)); + } + } + } + } + } + } + } + } + + if (checkTable.isComplete()) { + return PrivilegesEvaluatorResponse.ok(); + } + + Set availableIndices = checkTable.getCompleteRows(); + + if (!availableIndices.isEmpty()) { + return PrivilegesEvaluatorResponse.partiallyOk(availableIndices, checkTable).evaluationExceptions(exceptions); + } + + return PrivilegesEvaluatorResponse.insufficient(checkTable) + .reason( + resolvedIndices.getAllIndices().size() == 1 + ? "Insufficient permissions for the referenced index" + : "None of " + resolvedIndices.getAllIndices().size() + " referenced indices has sufficient permissions" + ) + .evaluationExceptions(exceptions); + } + + /** + * Returns PrivilegesEvaluatorResponse.ok() if the user identified in the context object has privileges for all + * indices (using *) for the given actions. Returns null otherwise. Then, further checks must be done to check + * the user's privileges. + */ + PrivilegesEvaluatorResponse providesWildcardPrivilege(PrivilegesEvaluationContext context, Set actions) { + ImmutableSet effectiveRoles = context.getMappedRoles(); + + for (String action : actions) { + ImmutableCompactSubSet rolesWithWildcardIndexPrivileges = this.actionToRolesWithWildcardIndexPrivileges.get(action); + + if (rolesWithWildcardIndexPrivileges == null || !rolesWithWildcardIndexPrivileges.containsAny(effectiveRoles)) { + return null; + } + } + + return PrivilegesEvaluatorResponse.ok(); + } + + /** + * Checks whether this instance provides explicit privileges for the combination of the provided action, + * the provided indices and the provided roles. + *

+ * Explicit means here that the privilege is not granted via a "*" action privilege wildcard. Other patterns + * are possible. See also: https://github.com/opensearch-project/security/pull/2411 and https://github.com/opensearch-project/security/issues/3038 + */ + PrivilegesEvaluatorResponse providesExplicitPrivilege( + PrivilegesEvaluationContext context, + Set actions, + IndexResolverReplacer.Resolved resolvedIndices, + CheckTable checkTable, + Map indexMetadata + ) { + List exceptions = new ArrayList<>(); + + if (!CollectionUtils.containsAny(actions, this.explicitlyRequiredIndexActions)) { + return PrivilegesEvaluatorResponse.insufficient(CheckTable.create(ImmutableSet.of("_"), actions)); + } + + for (String role : context.getMappedRoles()) { + ImmutableMap actionToIndexPattern = this.rolesToExplicitActionToIndexPattern.get(role); + + if (actionToIndexPattern != null) { + for (String action : actions) { + IndexPattern indexPattern = actionToIndexPattern.get(action); + + if (indexPattern != null) { + for (String index : checkTable.iterateUncheckedRows(action)) { + try { + if (indexPattern.matches(index, context, indexMetadata) && checkTable.check(index, action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } catch (PrivilegesEvaluationException e) { + // We can ignore these errors, as this max leads to fewer privileges than available + log.error("Error while evaluating index pattern of role {}. Ignoring entry", role, e); + exceptions.add(new PrivilegesEvaluationException("Error while evaluating role " + role, e)); + } + } + } + } + } + } + + return PrivilegesEvaluatorResponse.insufficient(checkTable) + .reason("No explicit privileges have been provided for the referenced indices.") + .evaluationExceptions(exceptions); + } + } + + /** + * Fully pre-computed, optimized index privilege maps. + *

+ * The data structures in this class are optimized to answer the question "given an action, an index and a set of + * roles, do I have the respective privilege" in O(1). + *

+ * There are cases where this class will not be able to answer this question. These cases are the following: + * - The requested index does not exist (especially the case for create index actions) + * - The action is not well-known. + * - The indices used for pre-computing the data structures are not complete (possibly due to race conditions) + * - The role definition uses placeholders (like "${user.name}") in index patterns. + * - The role definition grants privileges to all indices (via "*") (these are omitted here for efficiency reasons). + * In such cases, the question needs to be answered by IndexPermissions (see above). + *

+ * This class also takes into account aliases and data streams. If a permission is granted on an alias, it will be + * automatically inherited by the indices it points to. The same holds for the backing indices of a data stream. + */ + static class StatefulIndexPrivileges { + + /** + * Maps concrete action names to concrete index names and then to the roles which provide privileges for the + * combination of action and index. This map can contain besides indices also names of data streams and aliases. + * For aliases and data streams, it will then contain both the actual alias/data stream and the backing indices. + */ + private final Map>> actionToIndexToRoles; + + /** + * The index information that was used to construct this instance. + */ + private final Map indices; + + /** + * The well known index actions that were used to construct this instance. + */ + private final ImmutableSet wellKnownIndexActions; + + private final int estimatedByteSize; + + private long metadataVersion; + + /** + * Creates pre-computed index privileges based on the given parameters. + *

+ * This constructor will not throw an exception if it encounters any invalid configuration (that is, + * in particular, unparseable regular expressions). Rather, it will just log an error. This is okay, as it + * just results in fewer available privileges. + */ + StatefulIndexPrivileges( + SecurityDynamicConfiguration roles, + FlattenedActionGroups actionGroups, + ImmutableSet wellKnownIndexActions, + Map indices, + long metadataVersion, + ByteSizeValue statefulIndexMaxHeapSize + ) { + Map< + String, + CompactMapGroupBuilder.MapBuilder>> actionToIndexToRoles = + new HashMap<>(); + DeduplicatingCompactSubSetBuilder roleSetBuilder = new DeduplicatingCompactSubSetBuilder<>( + roles.getCEntries().keySet() + ); + CompactMapGroupBuilder> indexMapBuilder = + new CompactMapGroupBuilder<>(indices.keySet(), (k2) -> roleSetBuilder.createSubSetBuilder()); + + // We iterate here through the present RoleV7 instances and nested through their "index_permissions" sections. + // During the loop, the actionToIndexToRoles map is being built. + // For that, action patterns from the role will be matched against the "well-known actions" to build + // a concrete action map and index patterns from the role will be matched against the present indices + // to build a concrete index map. + // + // The complexity of this loop is O(n*m) where n is dependent on the structure of the roles configuration + // and m is the number of matched indices. This formula does not take the loop through matchedActions in + // account, as this is bound by a constant number and thus does not need to be considered in the O() notation. + + top: for (Map.Entry entry : roles.getCEntries().entrySet()) { + try { + String roleName = entry.getKey(); + RoleV7 role = entry.getValue(); + + roleSetBuilder.next(roleName); + + for (RoleV7.Index indexPermissions : role.getIndex_permissions()) { + ImmutableSet permissions = actionGroups.resolve(indexPermissions.getAllowed_actions()); + + if (indexPermissions.getIndex_patterns().contains("*")) { + // Wildcard index patterns are handled in the static IndexPermissions object. + // This avoids having to build huge data structures - when a very easy shortcut is available. + continue; + } + + WildcardMatcher indexMatcher = IndexPattern.from(indexPermissions.getIndex_patterns()).getStaticPattern(); + + if (indexMatcher == WildcardMatcher.NONE) { + // The pattern is likely blank because there are only templated patterns. + // Index patterns with templates are not handled here, but in the static IndexPermissions object + continue; + } + + for (String permission : permissions) { + WildcardMatcher actionMatcher = WildcardMatcher.from(permission); + Collection matchedActions = actionMatcher.getMatchAny(wellKnownIndexActions, Collectors.toList()); + + for (Map.Entry indicesEntry : indexMatcher.iterateMatching( + indices.entrySet(), + Map.Entry::getKey + )) { + for (String action : matchedActions) { + CompactMapGroupBuilder.MapBuilder< + String, + DeduplicatingCompactSubSetBuilder.SubSetBuilder> indexToRoles = actionToIndexToRoles + .computeIfAbsent(action, k -> indexMapBuilder.createMapBuilder()); + + indexToRoles.get(indicesEntry.getKey()).add(roleName); + + if (indicesEntry.getValue() instanceof IndexAbstraction.Alias) { + // For aliases we additionally add the sub-indices to the privilege map + for (IndexMetadata subIndex : indicesEntry.getValue().getIndices()) { + indexToRoles.get(subIndex.getIndex().getName()).add(roleName); + } + } + + if (roleSetBuilder.getEstimatedByteSize() + indexMapBuilder + .getEstimatedByteSize() > statefulIndexMaxHeapSize.getBytes()) { + log.info( + "Size of precomputed index privileges exceeds configured limit ({}). Using capped data structure." + + "This might lead to slightly lower performance during privilege evaluation. Consider raising {}.", + statefulIndexMaxHeapSize, + PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE.getKey() + ); + break top; + } + } + } + } + } + } catch (Exception e) { + log.error("Unexpected exception while processing role: {}\nIgnoring role.", entry.getKey(), e); + } + } + + DeduplicatingCompactSubSetBuilder.Completed completedRoleSetBuilder = roleSetBuilder.build(); + + this.estimatedByteSize = roleSetBuilder.getEstimatedByteSize() + indexMapBuilder.getEstimatedByteSize(); + log.debug("Estimated size of StatefulIndexPermissions data structure: {}", this.estimatedByteSize); + + this.actionToIndexToRoles = actionToIndexToRoles.entrySet() + .stream() + .collect( + ImmutableMap.toImmutableMap( + Map.Entry::getKey, + entry -> entry.getValue().build(subSetBuilder -> subSetBuilder.build(completedRoleSetBuilder)) + ) + ); + + this.indices = ImmutableMap.copyOf(indices); + this.metadataVersion = metadataVersion; + this.wellKnownIndexActions = wellKnownIndexActions; + } + + /** + * Checks whether the user has privileges based on the given parameters and information in this class. This method + * has two major channels for returning results: + *

+ * 1. The return value is either PrivilegesEvaluatorResponse.ok() or null. If it is null, this method cannot + * completely tell whether the user has full privileges. A further check with IndexPermissions will be necessary. + * If PrivilegesEvaluatorResponse.ok() is returned, then full privileges could be already determined. + *

+ * 2. As a side effect, this method will modify the supplied CheckTable object. This will be the case regardless + * of whether null or PrivilegesEvaluatorResponse.ok() is returned. The interesting case is actually when null + * is returned, because then the remaining logic needs only to check for the unchecked cases. + * + * @param actions the actions the user needs to have privileges for + * @param resolvedIndices the index the user needs to have privileges for + * @param context context information like user, resolved roles, etc. + * @param checkTable An action/index matrix. This method will modify the table as a side effect and check the cells where privileges are present. + * @return PrivilegesEvaluatorResponse.ok() or null. + */ + PrivilegesEvaluatorResponse providesPrivilege( + Set actions, + IndexResolverReplacer.Resolved resolvedIndices, + PrivilegesEvaluationContext context, + CheckTable checkTable, + Map indexMetadata + ) { + ImmutableSet effectiveRoles = context.getMappedRoles(); + + for (String action : actions) { + Map> indexToRoles = actionToIndexToRoles.get(action); + + if (indexToRoles != null) { + for (String index : resolvedIndices.getAllIndices()) { + String lookupIndex = index; + + if (index.startsWith(DataStream.BACKING_INDEX_PREFIX)) { + // If we have a backing index of a data stream, we will not try to test + // the backing index here, as we filter backing indices during initialization. + // Instead, we look up the containing data stream and check whether this has privileges. + lookupIndex = backingIndexToDataStream(index, indexMetadata); + } + + ImmutableCompactSubSet rolesWithPrivileges = indexToRoles.get(lookupIndex); + + if (rolesWithPrivileges != null && rolesWithPrivileges.containsAny(effectiveRoles)) { + if (checkTable.check(index, action)) { + return PrivilegesEvaluatorResponse.ok(); + } + } + } + } + } + + // If we reached this point, we cannot tell whether the user has privileges using this instance. + // Return null to indicate that there is no answer. + // The checkTable object might contain already a partial result. + return null; + } + + /** + * If the given index is the backing index of a data stream, the name of the data stream is returned. + * Otherwise, the name of the index itself is being returned. + */ + static String backingIndexToDataStream(String index, Map indexMetadata) { + IndexAbstraction indexAbstraction = indexMetadata.get(index); + + if (indexAbstraction instanceof IndexAbstraction.Index && indexAbstraction.getParentDataStream() != null) { + return indexAbstraction.getParentDataStream().getName(); + } else { + return index; + } + } + + /** + * Filters the given index abstraction map to only contain entries that are relevant the for stateful class. + * This has the goal to keep the heap footprint of instances of StatefulIndexPrivileges at a reasonable size. + *

+ * This removes the following entries: + *

    + *
  • closed indices - closed indices do not need any fast privilege evaluation + *
  • backing indices of data streams - privileges should be only assigned directly to the data streams. + * the privilege evaluation code is able to recognize that an index is member of a data stream and test + * its privilege via that data stream. If a privilege is directly assigned to a backing index, we use + * the "slowish" code paths. + *
  • Indices which are not matched by includeIndices + *
+ */ + static Map relevantOnly(Map indices) { + // First pass: Check if we need to filter at all + boolean doFilter = false; + + for (IndexAbstraction indexAbstraction : indices.values()) { + if (indexAbstraction instanceof IndexAbstraction.Index) { + if (indexAbstraction.getParentDataStream() != null + || indexAbstraction.getWriteIndex().getState() == IndexMetadata.State.CLOSE) { + doFilter = true; + break; + } + } + } + + if (!doFilter) { + return indices; + } + + // Second pass: Only if we actually need filtering, we will do it + ImmutableMap.Builder builder = ImmutableMap.builder(); + + for (IndexAbstraction indexAbstraction : indices.values()) { + if (indexAbstraction instanceof IndexAbstraction.Index) { + if (indexAbstraction.getParentDataStream() == null + && indexAbstraction.getWriteIndex().getState() != IndexMetadata.State.CLOSE) { + builder.put(indexAbstraction.getName(), indexAbstraction); + } + } else { + builder.put(indexAbstraction.getName(), indexAbstraction); + } + } + + return builder.build(); + } + } + +} diff --git a/src/main/java/org/opensearch/security/privileges/ClusterStateMetadataDependentPrivileges.java b/src/main/java/org/opensearch/security/privileges/ClusterStateMetadataDependentPrivileges.java new file mode 100644 index 0000000000..282e2e6bb6 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/ClusterStateMetadataDependentPrivileges.java @@ -0,0 +1,98 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.security.privileges; + +import java.util.concurrent.Future; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.threadpool.ThreadPool; + +/** + * Abstract super class for classes which need metadata updates from the cluster state. This class implements + * asynchronous updates - that means that any subclass needs to be prepared for not having the most up to date + * cluster state. + */ +public abstract class ClusterStateMetadataDependentPrivileges { + + private static final Logger log = LogManager.getLogger(ClusterStateMetadataDependentPrivileges.class); + private Future updateFuture; + + /** + * Updates the stateful index configuration asynchronously with the index metadata from the current cluster state. + * As the update process can take some seconds for clusters with many indices, this method "de-bounces" the updates, + * i.e., a further update will be only initiated after the previous update has finished. This is okay as this class + * can handle the case that it do not have the most recent information. It will fall back to slower methods then. + */ + public synchronized void updateClusterStateMetadataAsync(ClusterService clusterService, ThreadPool threadPool) { + long currentMetadataVersion = clusterService.state().metadata().version(); + + if (currentMetadataVersion <= getCurrentlyUsedMetadataVersion()) { + return; + } + + if (this.updateFuture == null || this.updateFuture.isDone()) { + this.updateFuture = threadPool.generic().submit(() -> { + for (int i = 0;; i++) { + if (i > 5) { + try { + // In case we got many consecutive updates, let's sleep a little to let + // other operations catch up. + Thread.sleep(100); + } catch (InterruptedException e) { + return; + } + } + + Metadata metadata = clusterService.state().metadata(); + + synchronized (ClusterStateMetadataDependentPrivileges.this) { + if (metadata.version() <= ClusterStateMetadataDependentPrivileges.this.getCurrentlyUsedMetadataVersion()) { + return; + } + } + + try { + log.debug("Updating {} with metadata version {}", this, metadata.version()); + updateClusterStateMetadata(metadata); + } catch (Exception e) { + log.error("Error while updating {}", this, e); + } finally { + synchronized (ClusterStateMetadataDependentPrivileges.this) { + if (ClusterStateMetadataDependentPrivileges.this.updateFuture.isCancelled()) { + // This can happen if this instance got obsolete due to a config update + // or if the node is shutting down + return; + } + } + } + } + }); + } + } + + /** + * Stops any concurrent update tasks to let the node gracefully shut down. + */ + public synchronized void shutdown() { + if (this.updateFuture != null && !this.updateFuture.isDone()) { + this.updateFuture.cancel(true); + } + } + + protected abstract void updateClusterStateMetadata(Metadata metadata); + + protected abstract long getCurrentlyUsedMetadataVersion(); + +} diff --git a/src/main/java/org/opensearch/security/privileges/ExpressionEvaluationException.java b/src/main/java/org/opensearch/security/privileges/ExpressionEvaluationException.java new file mode 100644 index 0000000000..50e933246b --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/ExpressionEvaluationException.java @@ -0,0 +1,21 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.security.privileges; + +/** + * This exception indicates that an expression - such as a regular expression - could not be properly evaluated during + * privilege evaluation. + */ +public class ExpressionEvaluationException extends Exception { + public ExpressionEvaluationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/opensearch/security/privileges/IndexPattern.java b/src/main/java/org/opensearch/security/privileges/IndexPattern.java new file mode 100644 index 0000000000..5b73904d14 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/IndexPattern.java @@ -0,0 +1,258 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.security.privileges; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import com.google.common.collect.ImmutableList; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.cluster.metadata.IndexAbstraction; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.security.support.WildcardMatcher; + +/** + * Aggregates index patterns defined in roles and segments them into patterns using template expressions ("index_${user.name}"), + * patterns using date math and plain patterns. This segmentation is needed because only plain patterns can be used + * to pre-compute privilege maps. The other types of patterns need to be evaluated "live" during the actual request. + */ +public class IndexPattern { + private static final Logger log = LogManager.getLogger(IndexPattern.class); + + /** + * An IndexPattern which does not match any index. + */ + public static final IndexPattern EMPTY = new IndexPattern(WildcardMatcher.NONE, ImmutableList.of(), ImmutableList.of()); + + /** + * Plain index patterns without any dynamic expressions like user attributes and date math. + * This can be not null. If this instance cannot match any static pattern, this will be WildcardMatcher.NONE. + */ + private final WildcardMatcher staticPattern; + + /** + * Index patterns which contain user attributes (like ${user.name}) + */ + private final ImmutableList patternTemplates; + + /** + * Index patterns which contain date math (like ) + */ + private final ImmutableList dateMathExpressions; + private final int hashCode; + + private IndexPattern(WildcardMatcher staticPattern, ImmutableList patternTemplates, ImmutableList dateMathExpressions) { + this.staticPattern = staticPattern; + this.patternTemplates = patternTemplates; + this.dateMathExpressions = dateMathExpressions; + this.hashCode = staticPattern.hashCode() + patternTemplates.hashCode() + dateMathExpressions.hashCode(); + } + + public boolean matches(String index, PrivilegesEvaluationContext context, Map indexMetadata) + throws PrivilegesEvaluationException { + if (staticPattern != WildcardMatcher.NONE && staticPattern.test(index)) { + return true; + } + + if (!patternTemplates.isEmpty()) { + for (String patternTemplate : this.patternTemplates) { + try { + WildcardMatcher matcher = context.getRenderedMatcher(patternTemplate); + + if (matcher.test(index)) { + return true; + } + } catch (ExpressionEvaluationException e) { + throw new PrivilegesEvaluationException("Error while evaluating dynamic index pattern: " + patternTemplate, e); + } + } + } + + if (!dateMathExpressions.isEmpty()) { + IndexNameExpressionResolver indexNameExpressionResolver = context.getIndexNameExpressionResolver(); + + // Note: The use of date math expressions in privileges is a bit odd, as it only provides a very limited + // solution for the potential user case. A different approach might be nice. + + for (String dateMathExpression : this.dateMathExpressions) { + try { + String resolvedExpression = indexNameExpressionResolver.resolveDateMathExpression(dateMathExpression); + + WildcardMatcher matcher = WildcardMatcher.from(resolvedExpression); + + if (matcher.test(index)) { + return true; + } + } catch (Exception e) { + throw new PrivilegesEvaluationException("Error while evaluating date math expression: " + dateMathExpression, e); + } + } + } + + IndexAbstraction indexAbstraction = indexMetadata.get(index); + + if (indexAbstraction instanceof IndexAbstraction.Index) { + // Check for the privilege for aliases or data streams containing this index + + if (indexAbstraction.getParentDataStream() != null) { + if (matches(indexAbstraction.getParentDataStream().getName(), context, indexMetadata)) { + return true; + } + } + + // Retrieve aliases: The use of getWriteIndex() is a bit messy, but it is the only way to access + // alias metadata from here. + for (String alias : indexAbstraction.getWriteIndex().getAliases().keySet()) { + if (matches(alias, context, indexMetadata)) { + return true; + } + } + } + + return false; + } + + @Override + public String toString() { + if (patternTemplates.size() == 0 && dateMathExpressions.size() == 0) { + return staticPattern.toString(); + } else { + StringBuilder result = new StringBuilder(); + + if (staticPattern != WildcardMatcher.NONE) { + result.append(staticPattern); + } + + if (patternTemplates.size() != 0) { + if (result.length() != 0) { + result.append(" "); + } + + result.append(String.join(",", patternTemplates)); + } + + if (dateMathExpressions.size() != 0) { + if (result.length() != 0) { + result.append(" "); + } + + result.append(String.join(",", dateMathExpressions)); + } + + return result.toString(); + } + } + + public WildcardMatcher getStaticPattern() { + return staticPattern; + } + + /** + * Returns true if this object contains patterns which can be matched against indices upfront. + */ + public boolean hasStaticPattern() { + return staticPattern != WildcardMatcher.NONE; + } + + /** + * Returns true if this object contains patterns which must be matched against indices again for each request, + * as they depend on user attributes or on the current time. + */ + public boolean hasDynamicPattern() { + return !patternTemplates.isEmpty() || !dateMathExpressions.isEmpty(); + } + + /** + * Returns a sub-set of this object, which includes only the patterns which must be matched against indices again for each request, + * as they depend on user attributes or on the current time. + */ + public IndexPattern dynamicOnly() { + if (patternTemplates.isEmpty() && dateMathExpressions.isEmpty()) { + return EMPTY; + } else { + return new IndexPattern(WildcardMatcher.NONE, this.patternTemplates, this.dateMathExpressions); + } + } + + /** + * Returns true if this object cannot match against any index name. + */ + public boolean isEmpty() { + return !hasStaticPattern() && !hasDynamicPattern(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof IndexPattern)) return false; + IndexPattern that = (IndexPattern) o; + return Objects.equals(staticPattern, that.staticPattern) + && Objects.equals(patternTemplates, that.patternTemplates) + && Objects.equals(dateMathExpressions, that.dateMathExpressions); + } + + @Override + public int hashCode() { + return hashCode; + } + + static class Builder { + private List constantPatterns = new ArrayList<>(); + private List patternTemplates = new ArrayList<>(); + private List dateMathExpressions = new ArrayList<>(); + + void add(List source) { + for (int i = 0; i < source.size(); i++) { + try { + String indexPattern = source.get(i); + + if (indexPattern.startsWith("<") && indexPattern.endsWith(">")) { + this.dateMathExpressions.add(indexPattern); + } else if (!containsPlaceholder(indexPattern)) { + this.constantPatterns.add(WildcardMatcher.from(indexPattern)); + } else { + this.patternTemplates.add(indexPattern); + } + } catch (Exception e) { + // This usually happens when the index pattern defines an unparseable regular expression + log.error("Error while creating index pattern for {}", source, e); + } + } + } + + IndexPattern build() { + return new IndexPattern( + constantPatterns.size() != 0 ? WildcardMatcher.from(constantPatterns) : WildcardMatcher.NONE, + ImmutableList.copyOf(patternTemplates), + ImmutableList.copyOf(dateMathExpressions) + ); + } + } + + static boolean containsPlaceholder(String indexPattern) { + return indexPattern.indexOf("${") != -1; + } + + public static IndexPattern from(List source) { + Builder builder = new Builder(); + builder.add(source); + return builder.build(); + } + + public static IndexPattern from(String... source) { + return from(Arrays.asList(source)); + } +} diff --git a/src/main/java/org/opensearch/security/privileges/PitPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PitPrivilegesEvaluator.java index 57c1c18414..4fd4141b08 100644 --- a/src/main/java/org/opensearch/security/privileges/PitPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PitPrivilegesEvaluator.java @@ -18,17 +18,15 @@ import java.util.Set; import java.util.concurrent.TimeUnit; +import com.google.common.collect.ImmutableSet; + import org.opensearch.action.ActionRequest; import org.opensearch.action.admin.indices.segments.PitSegmentsRequest; import org.opensearch.action.search.CreatePitRequest; import org.opensearch.action.search.DeletePitRequest; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.unit.TimeValue; import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.security.resolver.IndexResolverReplacer; -import org.opensearch.security.securityconf.SecurityRoles; -import org.opensearch.security.user.User; /** * This class evaluates privileges for point in time (Delete and List all) operations. @@ -39,11 +37,9 @@ public class PitPrivilegesEvaluator { public PrivilegesEvaluatorResponse evaluate( final ActionRequest request, - final ClusterService clusterService, - final User user, - final SecurityRoles securityRoles, + final PrivilegesEvaluationContext context, + final ActionPrivileges actionPrivileges, final String action, - final IndexNameExpressionResolver resolver, final PrivilegesEvaluatorResponse presponse, final IndexResolverReplacer irr ) { @@ -64,7 +60,7 @@ public PrivilegesEvaluatorResponse evaluate( if (pitIds.size() == 1 && "_all".equals(pitIds.get(0))) { return presponse; } else { - return handlePitsAccess(pitIds, clusterService, user, securityRoles, action, resolver, presponse, irr); + return handlePitsAccess(pitIds, context, actionPrivileges, action, presponse, irr); } } @@ -73,11 +69,9 @@ public PrivilegesEvaluatorResponse evaluate( */ private PrivilegesEvaluatorResponse handlePitsAccess( List pitIds, - ClusterService clusterService, - User user, - SecurityRoles securityRoles, + PrivilegesEvaluationContext context, + ActionPrivileges actionPrivileges, final String action, - IndexNameExpressionResolver resolver, PrivilegesEvaluatorResponse presponse, final IndexResolverReplacer irr ) { @@ -87,30 +81,16 @@ private PrivilegesEvaluatorResponse handlePitsAccess( for (String[] indices : pitToIndicesMap.values()) { pitIndices.addAll(Arrays.asList(indices)); } - Set allPermittedIndices = getPermittedIndices(pitIndices, clusterService, user, securityRoles, action, resolver, irr); + String[] indicesArr = new String[pitIndices.size()]; + CreatePitRequest req = new CreatePitRequest(new TimeValue(1, TimeUnit.DAYS), true, pitIndices.toArray(indicesArr)); + final IndexResolverReplacer.Resolved pitResolved = irr.resolveRequest(req); + PrivilegesEvaluatorResponse subResponse = actionPrivileges.hasIndexPrivilege(context, ImmutableSet.of(action), pitResolved); // Only if user has access to all PIT's indices, allow operation, otherwise continue evaluation in PrivilegesEvaluator. - if (allPermittedIndices.containsAll(pitIndices)) { + if (subResponse.isAllowed()) { presponse.allowed = true; presponse.markComplete(); } - return presponse; - } - /** - * This method returns list of permitted indices for the PIT indices passed - */ - private Set getPermittedIndices( - Set pitIndices, - ClusterService clusterService, - User user, - SecurityRoles securityRoles, - final String action, - IndexNameExpressionResolver resolver, - final IndexResolverReplacer irr - ) { - String[] indicesArr = new String[pitIndices.size()]; - CreatePitRequest req = new CreatePitRequest(new TimeValue(1, TimeUnit.DAYS), true, pitIndices.toArray(indicesArr)); - final IndexResolverReplacer.Resolved pitResolved = irr.resolveRequest(req); - return securityRoles.reduce(pitResolved, user, new String[] { action }, resolver, clusterService); + return presponse; } } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java index 98ffddb3d3..23771caa27 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java @@ -10,19 +10,26 @@ */ package org.opensearch.security.privileges; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + import com.google.common.collect.ImmutableSet; import org.opensearch.action.ActionRequest; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; /** * Request-scoped context information for privilege evaluation. - * + *

* This class carries metadata about the request and provides caching facilities for data which might need to be * evaluated several times per request. - * + *

* As this class is request-scoped, it is only used by a single thread. Thus, no thread synchronization mechanisms * are necessary. */ @@ -34,6 +41,15 @@ public class PrivilegesEvaluationContext { private final Task task; private ImmutableSet mappedRoles; private final IndexResolverReplacer indexResolverReplacer; + private final IndexNameExpressionResolver indexNameExpressionResolver; + private final Supplier clusterStateSupplier; + + /** + * This caches the ready to use WildcardMatcher instances for the current request. Many index patterns have + * to be executed several times per request (for example first for action privileges, later for DLS). Thus, + * it makes sense to cache and later re-use these. + */ + private final Map renderedPatternTemplateCache = new HashMap<>(); public PrivilegesEvaluationContext( User user, @@ -41,20 +57,49 @@ public PrivilegesEvaluationContext( String action, ActionRequest request, Task task, - IndexResolverReplacer indexResolverReplacer + IndexResolverReplacer indexResolverReplacer, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier clusterStateSupplier ) { this.user = user; this.mappedRoles = mappedRoles; this.action = action; this.request = request; - this.task = task; + this.clusterStateSupplier = clusterStateSupplier; this.indexResolverReplacer = indexResolverReplacer; + this.indexNameExpressionResolver = indexNameExpressionResolver; + this.task = task; } public User getUser() { return user; } + /** + * Interpolates any attribute references (like ${user.name}) in the given string and parses the result + * to a WildcardMatcher. This method catches earlier rendered templates in order to avoid recurring re-rendering + * of templates during a single privilege evaluation pass. + * + * @throws ExpressionEvaluationException if the resulting pattern could not be parsed. This is usually the case + * if an invalid regex was supplied. + */ + public WildcardMatcher getRenderedMatcher(String template) throws ExpressionEvaluationException { + WildcardMatcher matcher = this.renderedPatternTemplateCache.get(template); + + if (matcher == null) { + try { + matcher = WildcardMatcher.from(UserAttributes.replaceProperties(template, this)); + } catch (Exception e) { + // This especially happens for invalid regular expressions + throw new ExpressionEvaluationException("Error while evaluating expression in " + template, e); + } + + this.renderedPatternTemplateCache.put(template, matcher); + } + + return matcher; + } + public String getAction() { return action; } @@ -90,8 +135,15 @@ public ImmutableSet getMappedRoles() { * However, this method should be only used for this one particular phase. Normally, all roles should be determined * upfront and stay constant during the whole privilege evaluation process. */ - void setMappedRoles(ImmutableSet mappedRoles) { + public void setMappedRoles(ImmutableSet mappedRoles) { this.mappedRoles = mappedRoles; } + public Supplier getClusterStateSupplier() { + return clusterStateSupplier; + } + + public IndexNameExpressionResolver getIndexNameExpressionResolver() { + return indexNameExpressionResolver; + } } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationException.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationException.java new file mode 100644 index 0000000000..b3a0ac569f --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationException.java @@ -0,0 +1,42 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.security.privileges; + +import org.apache.commons.lang3.StringUtils; + +/** + * Signifies that an error was encountered while evaluating the privileges of a user for a particular request. + * + */ +public class PrivilegesEvaluationException extends Exception { + public PrivilegesEvaluationException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Returns a formatted multi-line-string showing cause messages as separate, indented lines. Does not include + * stack traces. + */ + public String getNestedMessages() { + if (this.getCause() == null) { + return this.getMessage(); + } + + StringBuilder result = new StringBuilder(this.getMessage()).append("\n"); + + Throwable cause = this.getCause(); + for (int i = 1; cause != null; cause = cause.getCause(), i++) { + result.append(StringUtils.repeat(' ', i * 3)).append(cause.getMessage()).append("\n"); + } + + return result.toString(); + } +} diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index fb320e6bd9..757a48866c 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -1,817 +1,54 @@ -/* - * Copyright 2015-2018 _floragunn_ GmbH - * Licensed 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. - */ - -/* - * 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. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - package org.opensearch.security.privileges; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.StringJoiner; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.OpenSearchSecurityException; import org.opensearch.action.ActionRequest; -import org.opensearch.action.IndicesRequest; -import org.opensearch.action.admin.cluster.shards.ClusterSearchShardsRequest; -import org.opensearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; -import org.opensearch.action.admin.indices.alias.IndicesAliasesAction; -import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; -import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; -import org.opensearch.action.admin.indices.create.AutoCreateAction; -import org.opensearch.action.admin.indices.create.CreateIndexAction; -import org.opensearch.action.admin.indices.create.CreateIndexRequest; -import org.opensearch.action.admin.indices.delete.DeleteIndexAction; -import org.opensearch.action.admin.indices.mapping.get.GetFieldMappingsRequest; -import org.opensearch.action.admin.indices.mapping.put.AutoPutMappingAction; -import org.opensearch.action.admin.indices.mapping.put.PutMappingAction; -import org.opensearch.action.bulk.BulkAction; -import org.opensearch.action.bulk.BulkItemRequest; -import org.opensearch.action.bulk.BulkRequest; -import org.opensearch.action.bulk.BulkShardRequest; -import org.opensearch.action.delete.DeleteAction; -import org.opensearch.action.get.GetRequest; -import org.opensearch.action.get.MultiGetAction; -import org.opensearch.action.index.IndexAction; -import org.opensearch.action.search.MultiSearchAction; -import org.opensearch.action.search.SearchAction; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchScrollAction; -import org.opensearch.action.support.IndicesOptions; -import org.opensearch.action.termvectors.MultiTermVectorsAction; -import org.opensearch.action.update.UpdateAction; -import org.opensearch.cluster.metadata.AliasMetadata; -import org.opensearch.cluster.metadata.IndexMetadata; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.core.common.Strings; +import org.opensearch.common.settings.Setting; import org.opensearch.core.common.transport.TransportAddress; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.index.reindex.ReindexAction; -import org.opensearch.script.mustache.RenderSearchTemplateAction; -import org.opensearch.security.auditlog.AuditLog; -import org.opensearch.security.configuration.ClusterInfoHolder; -import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.resolver.IndexResolverReplacer; -import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; -import org.opensearch.security.securityconf.ConfigModel; -import org.opensearch.security.securityconf.DynamicConfigModel; -import org.opensearch.security.securityconf.SecurityRoles; import org.opensearch.security.securityconf.impl.DashboardSignInOption; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; -import org.opensearch.threadpool.ThreadPool; - -import org.greenrobot.eventbus.Subscribe; -import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction; -import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT; - -public class PrivilegesEvaluator { - - static final WildcardMatcher DNFOF_MATCHER = WildcardMatcher.from( - ImmutableList.of( - "indices:data/read/*", - "indices:admin/mappings/fields/get*", - "indices:admin/shards/search_shards", - "indices:admin/resolve/index", - "indices:monitor/settings/get", - "indices:monitor/stats", - "indices:admin/aliases/get" - ) +public interface PrivilegesEvaluator { + static Setting USE_LEGACY_PRIVILEGE_EVALUATOR = Setting.boolSetting( + "plugins.security.privileges_evaluation.use_legacy_impl", + false, + Setting.Property.NodeScope ); - private static final WildcardMatcher ACTION_MATCHER = WildcardMatcher.from("indices:data/read/*search*"); - - private static final IndicesOptions ALLOW_EMPTY = IndicesOptions.fromOptions(true, true, false, false); - - protected final Logger log = LogManager.getLogger(this.getClass()); - private final ClusterService clusterService; - - private final IndexNameExpressionResolver resolver; - - private final AuditLog auditLog; - private ThreadContext threadContext; - - private PrivilegesInterceptor privilegesInterceptor; - - private final boolean checkSnapshotRestoreWritePrivileges; - - private final ClusterInfoHolder clusterInfoHolder; - private ConfigModel configModel; - private final IndexResolverReplacer irr; - private final SnapshotRestoreEvaluator snapshotRestoreEvaluator; - private final SystemIndexAccessEvaluator systemIndexAccessEvaluator; - private final ProtectedIndexAccessEvaluator protectedIndexAccessEvaluator; - private final TermsAggregationEvaluator termsAggregationEvaluator; - private final PitPrivilegesEvaluator pitPrivilegesEvaluator; - private DynamicConfigModel dcm; - private final NamedXContentRegistry namedXContentRegistry; - - public PrivilegesEvaluator( - final ClusterService clusterService, - final ThreadPool threadPool, - final ConfigurationRepository configurationRepository, - final IndexNameExpressionResolver resolver, - AuditLog auditLog, - final Settings settings, - final PrivilegesInterceptor privilegesInterceptor, - final ClusterInfoHolder clusterInfoHolder, - final IndexResolverReplacer irr, - NamedXContentRegistry namedXContentRegistry - ) { - - super(); - this.clusterService = clusterService; - this.resolver = resolver; - this.auditLog = auditLog; - - this.threadContext = threadPool.getThreadContext(); - this.privilegesInterceptor = privilegesInterceptor; - - this.checkSnapshotRestoreWritePrivileges = settings.getAsBoolean( - ConfigConstants.SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, - ConfigConstants.SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES - ); - - this.clusterInfoHolder = clusterInfoHolder; - this.irr = irr; - snapshotRestoreEvaluator = new SnapshotRestoreEvaluator(settings, auditLog); - systemIndexAccessEvaluator = new SystemIndexAccessEvaluator(settings, auditLog, irr); - protectedIndexAccessEvaluator = new ProtectedIndexAccessEvaluator(settings, auditLog); - termsAggregationEvaluator = new TermsAggregationEvaluator(); - pitPrivilegesEvaluator = new PitPrivilegesEvaluator(); - this.namedXContentRegistry = namedXContentRegistry; - } - - @Subscribe - public void onConfigModelChanged(ConfigModel configModel) { - this.configModel = configModel; - } - - @Subscribe - public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { - this.dcm = dcm; - } - - public SecurityRoles getSecurityRoles(Set roles) { - return configModel.getSecurityRoles().filter(roles); - } - - public boolean hasRestAdminPermissions(final User user, final TransportAddress remoteAddress, final String permissions) { - final Set userRoles = mapRoles(user, remoteAddress); - return hasRestAdminPermissions(userRoles, permissions); - } - - private boolean hasRestAdminPermissions(final Set roles, String permission) { - final SecurityRoles securityRoles = getSecurityRoles(roles); - return securityRoles.hasExplicitClusterPermissionPermission(permission); - } - - public boolean isInitialized() { - return configModel != null && configModel.getSecurityRoles() != null && dcm != null; - } - - private void setUserInfoInThreadContext(User user) { - if (threadContext.getTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT) == null) { - StringJoiner joiner = new StringJoiner("|"); - joiner.add(user.getName()); - joiner.add(String.join(",", user.getRoles())); - joiner.add(String.join(",", user.getSecurityRoles())); - String requestedTenant = user.getRequestedTenant(); - if (!Strings.isNullOrEmpty(requestedTenant)) { - joiner.add(requestedTenant); - } - threadContext.putTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT, joiner.toString()); - } - } - - public PrivilegesEvaluationContext createContext( - User user, - String action0, - ActionRequest request, - Task task, - Set injectedRoles - ) { - if (!isInitialized()) { - throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); - } - - TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); - ImmutableSet mappedRoles = ImmutableSet.copyOf((injectedRoles == null) ? mapRoles(user, caller) : injectedRoles); - - return new PrivilegesEvaluationContext(user, mappedRoles, action0, request, task, irr); - } - - public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { - - if (!isInitialized()) { - throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); - } - - String action0 = context.getAction(); - ImmutableSet mappedRoles = context.getMappedRoles(); - User user = context.getUser(); - ActionRequest request = context.getRequest(); - Task task = context.getTask(); - - if (action0.startsWith("internal:indices/admin/upgrade")) { - action0 = "indices:admin/upgrade"; - } - - if (AutoCreateAction.NAME.equals(action0)) { - action0 = CreateIndexAction.NAME; - } - - if (AutoPutMappingAction.NAME.equals(action0)) { - action0 = PutMappingAction.NAME; - } - - final PrivilegesEvaluatorResponse presponse = new PrivilegesEvaluatorResponse(); - - final String injectedRolesValidationString = threadContext.getTransient( - ConfigConstants.OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION - ); - if (injectedRolesValidationString != null) { - HashSet injectedRolesValidationSet = new HashSet<>(Arrays.asList(injectedRolesValidationString.split(","))); - if (!mappedRoles.containsAll(injectedRolesValidationSet)) { - presponse.allowed = false; - presponse.missingSecurityRoles.addAll(injectedRolesValidationSet); - log.info("Roles {} are not mapped to the user {}", injectedRolesValidationSet, user); - return presponse; - } - mappedRoles = ImmutableSet.copyOf(injectedRolesValidationSet); - context.setMappedRoles(mappedRoles); - } - presponse.resolvedSecurityRoles.addAll(mappedRoles); - final SecurityRoles securityRoles = getSecurityRoles(mappedRoles); - - // Add the security roles for this user so that they can be used for DLS parameter substitution. - user.addSecurityRoles(mappedRoles); - setUserInfoInThreadContext(user); - - final boolean isDebugEnabled = log.isDebugEnabled(); - if (isDebugEnabled) { - log.debug("Evaluate permissions for {} on {}", user, clusterService.localNode().getName()); - log.debug("Action: {} ({})", action0, request.getClass().getSimpleName()); - log.debug("Mapped roles: {}", mappedRoles.toString()); - } - - if (request instanceof BulkRequest && (Strings.isNullOrEmpty(user.getRequestedTenant()))) { - // Shortcut for bulk actions. The details are checked on the lower level of the BulkShardRequests (Action - // indices:data/write/bulk[s]). - // This shortcut is only possible if the default tenant is selected, as we might need to rewrite the request for non-default - // tenants. - // No further access check for the default tenant is necessary, as access will be also checked on the TransportShardBulkAction - // level. - - if (!securityRoles.impliesClusterPermissionPermission(action0)) { - presponse.missingPrivileges.add(action0); - presponse.allowed = false; - log.info( - "No cluster-level perm match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}", - user, - action0, - mappedRoles, - presponse.missingPrivileges - ); - } else { - presponse.allowed = true; - } - return presponse; - } - - final Resolved requestedResolved = context.getResolvedRequest(); - - if (isDebugEnabled) { - log.debug("RequestedResolved : {}", requestedResolved); - } - - // check snapshot/restore requests - if (snapshotRestoreEvaluator.evaluate(request, task, action0, clusterInfoHolder, presponse).isComplete()) { - return presponse; - } - - // Security index access - if (systemIndexAccessEvaluator.evaluate( - request, - task, - action0, - requestedResolved, - presponse, - securityRoles, - user, - resolver, - clusterService - ).isComplete()) { - return presponse; - } - - // Protected index access - if (protectedIndexAccessEvaluator.evaluate(request, task, action0, requestedResolved, presponse, mappedRoles).isComplete()) { - return presponse; - } - - // check access for point in time requests - if (pitPrivilegesEvaluator.evaluate(request, clusterService, user, securityRoles, action0, resolver, presponse, irr).isComplete()) { - return presponse; - } - - final boolean dnfofEnabled = dcm.isDnfofEnabled(); - - final boolean isTraceEnabled = log.isTraceEnabled(); - if (isTraceEnabled) { - log.trace("dnfof enabled? {}", dnfofEnabled); - } - - final boolean serviceAccountUser = user.isServiceAccount(); - if (isClusterPerm(action0)) { - if (serviceAccountUser) { - presponse.missingPrivileges.add(action0); - presponse.allowed = false; - log.info("{} is a service account which doesn't have access to cluster level permission: {}", user, action0); - return presponse; - } - - if (!securityRoles.impliesClusterPermissionPermission(action0)) { - presponse.missingPrivileges.add(action0); - presponse.allowed = false; - log.info( - "No cluster-level perm match for {} {} [Action [{}]] [RolesChecked {}]. No permissions for {}", - user, - requestedResolved, - action0, - mappedRoles, - presponse.missingPrivileges - ); - return presponse; - } else { - - if (request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { - if (isDebugEnabled) { - log.debug("Normally allowed but we need to apply some extra checks for a restore request."); - } - } else { - if (privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { - - final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( - request, - action0, - user, - dcm, - requestedResolved, - mapTenants(user, mappedRoles) - ); - - if (isDebugEnabled) { - log.debug("Result from privileges interceptor for cluster perm: {}", replaceResult); - } - - if (!replaceResult.continueEvaluation) { - if (replaceResult.accessDenied) { - auditLog.logMissingPrivileges(action0, request, task); - } else { - presponse.allowed = true; - presponse.createIndexRequestBuilder = replaceResult.createIndexRequestBuilder; - } - return presponse; - } - } - - if (isDebugEnabled) { - log.debug("Allowed because we have cluster permissions for {}", action0); - } - presponse.allowed = true; - return presponse; - } - - } - } - - if (checkDocAllowListHeader(user, action0, request)) { - presponse.allowed = true; - return presponse; - } - - // term aggregations - if (termsAggregationEvaluator.evaluate(requestedResolved, request, clusterService, user, securityRoles, resolver, presponse) - .isComplete()) { - return presponse; - } - - final Set allIndexPermsRequired = evaluateAdditionalIndexPermissions(request, action0); - final String[] allIndexPermsRequiredA = allIndexPermsRequired.toArray(new String[0]); - - if (isDebugEnabled) { - log.debug( - "Requested {} from {}", - allIndexPermsRequired, - threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS) - ); - } - - presponse.missingPrivileges.clear(); - presponse.missingPrivileges.addAll(allIndexPermsRequired); - - if (isDebugEnabled) { - log.debug("Requested resolved index types: {}", requestedResolved); - log.debug("Security roles: {}", mappedRoles); - } - - // TODO exclude Security index - - if (privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { - - final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( - request, - action0, - user, - dcm, - requestedResolved, - mapTenants(user, mappedRoles) - ); - - if (isDebugEnabled) { - log.debug("Result from privileges interceptor: {}", replaceResult); - } - - if (!replaceResult.continueEvaluation) { - if (replaceResult.accessDenied) { - auditLog.logMissingPrivileges(action0, request, task); - } else { - presponse.allowed = true; - presponse.createIndexRequestBuilder = replaceResult.createIndexRequestBuilder; - } - return presponse; - } - } - - if (dnfofEnabled && DNFOF_MATCHER.test(action0)) { - - if (requestedResolved.getAllIndices().isEmpty()) { - presponse.missingPrivileges.clear(); - presponse.allowed = true; - return presponse; - } - - Set reduced = securityRoles.reduce(requestedResolved, user, allIndexPermsRequiredA, resolver, clusterService); - - if (reduced.isEmpty()) { - if (dcm.isDnfofForEmptyResultsEnabled() && request instanceof IndicesRequest.Replaceable) { - - ((IndicesRequest.Replaceable) request).indices(new String[0]); - presponse.missingPrivileges.clear(); - presponse.allowed = true; - - if (request instanceof SearchRequest) { - ((SearchRequest) request).indicesOptions(ALLOW_EMPTY); - } else if (request instanceof ClusterSearchShardsRequest) { - ((ClusterSearchShardsRequest) request).indicesOptions(ALLOW_EMPTY); - } else if (request instanceof GetFieldMappingsRequest) { - ((GetFieldMappingsRequest) request).indicesOptions(ALLOW_EMPTY); - } - - return presponse; - } - presponse.allowed = false; - return presponse; - } - - if (irr.replace(request, true, reduced.toArray(new String[0]))) { - presponse.missingPrivileges.clear(); - presponse.allowed = true; - return presponse; - } - } - - // not bulk, mget, etc request here - boolean permGiven = false; - - if (isDebugEnabled) { - log.debug("Security roles: {}", securityRoles.getRoleNames()); - } - - if (dcm.isMultiRolespanEnabled()) { - permGiven = securityRoles.impliesTypePermGlobal(requestedResolved, user, allIndexPermsRequiredA, resolver, clusterService); - } else { - permGiven = securityRoles.get(requestedResolved, user, allIndexPermsRequiredA, resolver, clusterService); - - } - - if (!permGiven) { - log.info( - "No {}-level perm match for {} {} [Action [{}]] [RolesChecked {}]", - "index", - user, - requestedResolved, - action0, - mappedRoles - ); - log.info("No permissions for {}", presponse.missingPrivileges); - } else { - - if (checkFilteredAliases(requestedResolved, action0, isDebugEnabled)) { - presponse.allowed = false; - return presponse; - } - - if (isDebugEnabled) { - log.debug("Allowed because we have all indices permissions for {}", action0); - } - } - - presponse.allowed = permGiven; - return presponse; - - } - - public Set mapRoles(final User user, final TransportAddress caller) { - return this.configModel.mapSecurityRoles(user, caller); - } - - public Map mapTenants(final User user, Set roles) { - return this.configModel.mapTenants(user, roles); - } - - public Set getAllConfiguredTenantNames() { - - return configModel.getAllConfiguredTenantNames(); - } - - public boolean multitenancyEnabled() { - return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDashboardsMultitenancyEnabled(); - } - - public boolean privateTenantEnabled() { - return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDashboardsPrivateTenantEnabled(); - } - - public String dashboardsDefaultTenant() { - return dcm.getDashboardsDefaultTenant(); - } - - public boolean notFailOnForbiddenEnabled() { - return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDnfofEnabled(); - } - - public String dashboardsIndex() { - return dcm.getDashboardsIndexname(); - } - - public String dashboardsServerUsername() { - return dcm.getDashboardsServerUsername(); - } - - public String dashboardsOpenSearchRole() { - return dcm.getDashboardsOpenSearchRole(); - } - - public List getSignInOptions() { - return dcm.getSignInOptions(); - } - - private Set evaluateAdditionalIndexPermissions(final ActionRequest request, final String originalAction) { - // --- check inner bulk requests - final Set additionalPermissionsRequired = new HashSet<>(); - - if (!isClusterPerm(originalAction)) { - additionalPermissionsRequired.add(originalAction); - } - - if (request instanceof ClusterSearchShardsRequest) { - additionalPermissionsRequired.add(SearchAction.NAME); - } - - if (request instanceof BulkShardRequest) { - BulkShardRequest bsr = (BulkShardRequest) request; - for (BulkItemRequest bir : bsr.items()) { - switch (bir.request().opType()) { - case CREATE: - additionalPermissionsRequired.add(IndexAction.NAME); - break; - case INDEX: - additionalPermissionsRequired.add(IndexAction.NAME); - break; - case DELETE: - additionalPermissionsRequired.add(DeleteAction.NAME); - break; - case UPDATE: - additionalPermissionsRequired.add(UpdateAction.NAME); - break; - } - } - } - - if (request instanceof IndicesAliasesRequest) { - IndicesAliasesRequest bsr = (IndicesAliasesRequest) request; - for (AliasActions bir : bsr.getAliasActions()) { - switch (bir.actionType()) { - case REMOVE_INDEX: - additionalPermissionsRequired.add(DeleteIndexAction.NAME); - break; - default: - break; - } - } - } - - if (request instanceof CreateIndexRequest) { - CreateIndexRequest cir = (CreateIndexRequest) request; - if (cir.aliases() != null && !cir.aliases().isEmpty()) { - additionalPermissionsRequired.add(IndicesAliasesAction.NAME); - } - } - - if (request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { - additionalPermissionsRequired.addAll(ConfigConstants.SECURITY_SNAPSHOT_RESTORE_NEEDED_WRITE_PRIVILEGES); - } - - if (additionalPermissionsRequired.size() > 1) { - traceAction("Additional permissions required: {}", additionalPermissionsRequired); - } - - if (log.isDebugEnabled() && additionalPermissionsRequired.size() > 1) { - log.debug("Additional permissions required: {}", additionalPermissionsRequired); - } - - return Collections.unmodifiableSet(additionalPermissionsRequired); - } - - public static boolean isClusterPerm(String action0) { - return (action0.startsWith("cluster:") - || action0.startsWith("indices:admin/template/") - || action0.startsWith("indices:admin/index_template/") - || action0.startsWith(SearchScrollAction.NAME) - || (action0.equals(BulkAction.NAME)) - || (action0.equals(MultiGetAction.NAME)) - || (action0.startsWith(MultiSearchAction.NAME)) - || (action0.equals(MultiTermVectorsAction.NAME)) - || (action0.equals(ReindexAction.NAME)) - || (action0.equals(RenderSearchTemplateAction.NAME))); - } - - @SuppressWarnings("unchecked") - private boolean checkFilteredAliases(Resolved requestedResolved, String action, boolean isDebugEnabled) { - final String faMode = dcm.getFilteredAliasMode();// getConfigSettings().dynamic.filtered_alias_mode; - - if (!"disallow".equals(faMode)) { - return false; - } - - if (!ACTION_MATCHER.test(action)) { - return false; - } - - Iterable indexMetaDataCollection; - - if (requestedResolved.isLocalAll()) { - indexMetaDataCollection = new Iterable() { - @Override - public Iterator iterator() { - return clusterService.state().getMetadata().getIndices().values().iterator(); - } - }; - } else { - Set indexMetaDataSet = new HashSet<>(requestedResolved.getAllIndices().size()); - - for (String requestAliasOrIndex : requestedResolved.getAllIndices()) { - IndexMetadata indexMetaData = clusterService.state().getMetadata().getIndices().get(requestAliasOrIndex); - if (indexMetaData == null) { - if (isDebugEnabled) { - log.debug("{} does not exist in cluster metadata", requestAliasOrIndex); - } - continue; - } - - indexMetaDataSet.add(indexMetaData); - } - - indexMetaDataCollection = indexMetaDataSet; - } - // check filtered aliases - for (IndexMetadata indexMetaData : indexMetaDataCollection) { - - final List filteredAliases = new ArrayList(); - - final Map aliases = indexMetaData.getAliases(); + boolean hasRestAdminPermissions(final User user, final TransportAddress remoteAddress, final String permission); - if (aliases != null && aliases.size() > 0) { - if (isDebugEnabled) { - log.debug("Aliases for {}: {}", indexMetaData.getIndex().getName(), aliases); - } + boolean isInitialized(); - final Iterator it = aliases.keySet().iterator(); - while (it.hasNext()) { - final String alias = it.next(); - final AliasMetadata aliasMetadata = aliases.get(alias); + PrivilegesEvaluationContext createContext(User user, String action); - if (aliasMetadata != null && aliasMetadata.filteringRequired()) { - filteredAliases.add(aliasMetadata); - if (isDebugEnabled) { - log.debug("{} is a filtered alias {}", alias, aliasMetadata.getFilter()); - } - } else { - if (isDebugEnabled) { - log.debug("{} is not an alias or does not have a filter", alias); - } - } - } - } + PrivilegesEvaluationContext createContext(User user, String action0, ActionRequest request, Task task, Set injectedRoles); - if (filteredAliases.size() > 1 && ACTION_MATCHER.test(action)) { - // TODO add queries as dls queries (works only if dls module is installed) - log.error( - "More than one ({}) filtered alias found for same index ({}). This is currently not supported. Aliases: {}", - filteredAliases.size(), - indexMetaData.getIndex().getName(), - toString(filteredAliases) - ); - return true; - } - } // end-for + PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context); - return false; - } + Set mapRoles(final User user, final TransportAddress caller); - private boolean checkDocAllowListHeader(User user, String action, ActionRequest request) { - String docAllowListHeader = threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DOC_ALLOWLIST_HEADER); + Map mapTenants(final User user, Set roles); - if (docAllowListHeader == null) { - return false; - } + Set getAllConfiguredTenantNames(); - if (!(request instanceof GetRequest)) { - return false; - } + boolean multitenancyEnabled(); - try { - DocumentAllowList documentAllowList = DocumentAllowList.parse(docAllowListHeader); - GetRequest getRequest = (GetRequest) request; + boolean privateTenantEnabled(); - if (documentAllowList.isAllowed(getRequest.index(), getRequest.id())) { - if (log.isDebugEnabled()) { - log.debug("Request " + request + " is allowed by " + documentAllowList); - } + String dashboardsDefaultTenant(); - return true; - } else { - return false; - } + boolean notFailOnForbiddenEnabled(); - } catch (Exception e) { - log.error("Error while handling document allow list: " + docAllowListHeader, e); - return false; - } - } + String dashboardsIndex(); - private List toString(List aliases) { - if (aliases == null || aliases.size() == 0) { - return Collections.emptyList(); - } + String dashboardsServerUsername(); - final List ret = new ArrayList<>(aliases.size()); + String dashboardsOpenSearchRole(); - for (final AliasMetadata amd : aliases) { - if (amd != null) { - ret.add(amd.alias()); - } - } + List getSignInOptions(); - return Collections.unmodifiableList(ret); - } + PrivilegesEvaluatorResponse hasExplicitIndexPrivilege(PrivilegesEvaluationContext context, Set actions, String index); } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorImpl.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorImpl.java new file mode 100644 index 0000000000..c04119d415 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorImpl.java @@ -0,0 +1,846 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.privileges; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.action.ActionRequest; +import org.opensearch.action.IndicesRequest; +import org.opensearch.action.admin.cluster.shards.ClusterSearchShardsRequest; +import org.opensearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; +import org.opensearch.action.admin.indices.alias.IndicesAliasesAction; +import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; +import org.opensearch.action.admin.indices.create.AutoCreateAction; +import org.opensearch.action.admin.indices.create.CreateIndexAction; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.admin.indices.delete.DeleteIndexAction; +import org.opensearch.action.admin.indices.mapping.get.GetFieldMappingsRequest; +import org.opensearch.action.admin.indices.mapping.put.AutoPutMappingAction; +import org.opensearch.action.admin.indices.mapping.put.PutMappingAction; +import org.opensearch.action.bulk.BulkAction; +import org.opensearch.action.bulk.BulkItemRequest; +import org.opensearch.action.bulk.BulkRequest; +import org.opensearch.action.bulk.BulkShardRequest; +import org.opensearch.action.delete.DeleteAction; +import org.opensearch.action.get.GetRequest; +import org.opensearch.action.get.MultiGetAction; +import org.opensearch.action.index.IndexAction; +import org.opensearch.action.search.MultiSearchAction; +import org.opensearch.action.search.SearchAction; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchScrollAction; +import org.opensearch.action.support.IndicesOptions; +import org.opensearch.action.termvectors.MultiTermVectorsAction; +import org.opensearch.action.update.UpdateAction; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.AliasMetadata; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.common.Strings; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.index.reindex.ReindexAction; +import org.opensearch.script.mustache.RenderSearchTemplateAction; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.configuration.ClusterInfoHolder; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; +import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.securityconf.DynamicConfigFactory; +import org.opensearch.security.securityconf.DynamicConfigModel; +import org.opensearch.security.securityconf.FlattenedActionGroups; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.DashboardSignInOption; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.ActionGroupsV7; +import org.opensearch.security.securityconf.impl.v7.RoleV7; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.security.user.User; +import org.opensearch.tasks.Task; +import org.opensearch.threadpool.ThreadPool; + +import org.greenrobot.eventbus.Subscribe; + +import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction; +import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT; + +public class PrivilegesEvaluatorImpl implements PrivilegesEvaluator { + + static final WildcardMatcher DNFOF_MATCHER = WildcardMatcher.from( + ImmutableList.of( + "indices:data/read/*", + "indices:admin/mappings/fields/get*", + "indices:admin/shards/search_shards", + "indices:admin/resolve/index", + "indices:monitor/settings/get", + "indices:monitor/stats", + "indices:admin/aliases/get" + ) + ); + + private static final WildcardMatcher ACTION_MATCHER = WildcardMatcher.from("indices:data/read/*search*"); + + private static final IndicesOptions ALLOW_EMPTY = IndicesOptions.fromOptions(true, true, false, false); + + protected final Logger log = LogManager.getLogger(this.getClass()); + private final Supplier clusterStateSupplier; + + private final IndexNameExpressionResolver resolver; + + private final AuditLog auditLog; + private ThreadContext threadContext; + + private PrivilegesInterceptor privilegesInterceptor; + + private final boolean checkSnapshotRestoreWritePrivileges; + + private final ClusterInfoHolder clusterInfoHolder; + private ConfigModel configModel; + private final IndexResolverReplacer irr; + private final SnapshotRestoreEvaluator snapshotRestoreEvaluator; + private final SystemIndexAccessEvaluator systemIndexAccessEvaluator; + private final ProtectedIndexAccessEvaluator protectedIndexAccessEvaluator; + private final TermsAggregationEvaluator termsAggregationEvaluator; + private final PitPrivilegesEvaluator pitPrivilegesEvaluator; + private DynamicConfigModel dcm; + private final NamedXContentRegistry namedXContentRegistry; + private final Settings settings; + private final AtomicReference actionPrivileges = new AtomicReference<>(); + + public PrivilegesEvaluatorImpl( + final ClusterService clusterService, + Supplier clusterStateSupplier, + ThreadPool threadPool, + final ThreadContext threadContext, + final ConfigurationRepository configurationRepository, + final IndexNameExpressionResolver resolver, + AuditLog auditLog, + final Settings settings, + final PrivilegesInterceptor privilegesInterceptor, + final ClusterInfoHolder clusterInfoHolder, + final IndexResolverReplacer irr, + NamedXContentRegistry namedXContentRegistry + ) { + + super(); + this.resolver = resolver; + this.auditLog = auditLog; + + this.threadContext = threadContext; + this.privilegesInterceptor = privilegesInterceptor; + this.clusterStateSupplier = clusterStateSupplier; + this.settings = settings; + + this.checkSnapshotRestoreWritePrivileges = settings.getAsBoolean( + ConfigConstants.SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, + ConfigConstants.SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES + ); + + this.clusterInfoHolder = clusterInfoHolder; + this.irr = irr; + snapshotRestoreEvaluator = new SnapshotRestoreEvaluator(settings, auditLog); + systemIndexAccessEvaluator = new SystemIndexAccessEvaluator(settings, auditLog, irr); + protectedIndexAccessEvaluator = new ProtectedIndexAccessEvaluator(settings, auditLog); + termsAggregationEvaluator = new TermsAggregationEvaluator(); + pitPrivilegesEvaluator = new PitPrivilegesEvaluator(); + this.namedXContentRegistry = namedXContentRegistry; + + if (configurationRepository != null) { + configurationRepository.subscribeOnChange(configMap -> { + try { + SecurityDynamicConfiguration actionGroupsConfiguration = configurationRepository.getConfiguration( + CType.ACTIONGROUPS + ); + SecurityDynamicConfiguration rolesConfiguration = configurationRepository.getConfiguration(CType.ROLES); + + this.updateConfiguration(actionGroupsConfiguration, rolesConfiguration); + } catch (Exception e) { + log.error("Error while updating ActionPrivileges object with {}", configMap, e); + } + }); + } + + if (clusterService != null) { + clusterService.addListener(event -> { + ActionPrivileges actionPrivileges = PrivilegesEvaluatorImpl.this.actionPrivileges.get(); + if (actionPrivileges != null) { + actionPrivileges.updateClusterStateMetadataAsync(clusterService, threadPool); + } + }); + } + + } + + void updateConfiguration( + SecurityDynamicConfiguration actionGroupsConfiguration, + SecurityDynamicConfiguration rolesConfiguration + ) { + if (rolesConfiguration != null) { + SecurityDynamicConfiguration actionGroupsWithStatics = actionGroupsConfiguration != null + ? DynamicConfigFactory.addStatics(actionGroupsConfiguration.clone()) + : DynamicConfigFactory.addStatics(SecurityDynamicConfiguration.empty(CType.ACTIONGROUPS)); + FlattenedActionGroups flattenedActionGroups = new FlattenedActionGroups(actionGroupsWithStatics); + ActionPrivileges actionPrivileges = new ActionPrivileges( + DynamicConfigFactory.addStatics(rolesConfiguration.clone()), + flattenedActionGroups, + () -> clusterStateSupplier.get().metadata().getIndicesLookup(), + settings + ); + Metadata metadata = clusterStateSupplier.get().metadata(); + actionPrivileges.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), metadata.version()); + ActionPrivileges oldInstance = this.actionPrivileges.getAndSet(actionPrivileges); + + if (oldInstance != null) { + oldInstance.shutdown(); + } + } + } + + @Subscribe + public void onConfigModelChanged(ConfigModel configModel) { + this.configModel = configModel; + } + + @Subscribe + public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { + this.dcm = dcm; + } + + public ActionPrivileges getActionPrivileges() { + return this.actionPrivileges.get(); + } + + public boolean hasRestAdminPermissions(final User user, final TransportAddress remoteAddress, final String permission) { + PrivilegesEvaluationContext context = createContext(user, permission); + return this.actionPrivileges.get().hasExplicitClusterPrivilege(context, permission).isAllowed(); + } + + public boolean isInitialized() { + return configModel != null && dcm != null && actionPrivileges.get() != null; + } + + private void setUserInfoInThreadContext(User user) { + if (threadContext.getTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT) == null) { + StringJoiner joiner = new StringJoiner("|"); + joiner.add(user.getName()); + joiner.add(String.join(",", user.getRoles())); + joiner.add(String.join(",", user.getSecurityRoles())); + String requestedTenant = user.getRequestedTenant(); + if (!Strings.isNullOrEmpty(requestedTenant)) { + joiner.add(requestedTenant); + } + threadContext.putTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT, joiner.toString()); + } + } + + public PrivilegesEvaluationContext createContext(User user, String action) { + return createContext(user, action, null, null, null); + } + + public PrivilegesEvaluationContext createContext( + User user, + String action0, + ActionRequest request, + Task task, + Set injectedRoles + ) { + if (!isInitialized()) { + throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); + } + + TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + ImmutableSet mappedRoles = ImmutableSet.copyOf((injectedRoles == null) ? mapRoles(user, caller) : injectedRoles); + + return new PrivilegesEvaluationContext(user, mappedRoles, action0, request, task, irr, resolver, clusterStateSupplier); + } + + public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { + + if (!isInitialized()) { + throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); + } + + String action0 = context.getAction(); + ImmutableSet mappedRoles = context.getMappedRoles(); + User user = context.getUser(); + ActionRequest request = context.getRequest(); + Task task = context.getTask(); + + if (action0.startsWith("internal:indices/admin/upgrade")) { + action0 = "indices:admin/upgrade"; + } + + if (AutoCreateAction.NAME.equals(action0)) { + action0 = CreateIndexAction.NAME; + } + + if (AutoPutMappingAction.NAME.equals(action0)) { + action0 = PutMappingAction.NAME; + } + + PrivilegesEvaluatorResponse presponse = new PrivilegesEvaluatorResponse(); + + final String injectedRolesValidationString = threadContext.getTransient( + ConfigConstants.OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION + ); + if (injectedRolesValidationString != null) { + HashSet injectedRolesValidationSet = new HashSet<>(Arrays.asList(injectedRolesValidationString.split(","))); + if (!mappedRoles.containsAll(injectedRolesValidationSet)) { + presponse.allowed = false; + presponse.missingSecurityRoles.addAll(injectedRolesValidationSet); + log.info("Roles {} are not mapped to the user {}", injectedRolesValidationSet, user); + return presponse; + } + mappedRoles = ImmutableSet.copyOf(injectedRolesValidationSet); + context.setMappedRoles(mappedRoles); + } + + // Add the security roles for this user so that they can be used for DLS parameter substitution. + user.addSecurityRoles(mappedRoles); + setUserInfoInThreadContext(user); + + final boolean isDebugEnabled = log.isDebugEnabled(); + if (isDebugEnabled) { + log.debug("Evaluate permissions for {}", user); + log.debug("Action: {} ({})", action0, request.getClass().getSimpleName()); + log.debug("Mapped roles: {}", mappedRoles.toString()); + } + + ActionPrivileges actionPrivileges = this.actionPrivileges.get(); + if (actionPrivileges == null) { + throw new OpenSearchSecurityException("OpenSearch Security is not initialized: roles configuration is missing"); + } + + if (request instanceof BulkRequest && (Strings.isNullOrEmpty(user.getRequestedTenant()))) { + // Shortcut for bulk actions. The details are checked on the lower level of the BulkShardRequests (Action + // indices:data/write/bulk[s]). + // This shortcut is only possible if the default tenant is selected, as we might need to rewrite the request for non-default + // tenants. + // No further access check for the default tenant is necessary, as access will be also checked on the TransportShardBulkAction + // level. + + presponse = actionPrivileges.hasClusterPrivilege(context, action0); + + if (!presponse.allowed) { + log.info( + "No cluster-level perm match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}", + user, + action0, + mappedRoles, + presponse.getMissingPrivileges() + ); + } + return presponse; + } + + final Resolved requestedResolved = context.getResolvedRequest(); + + if (isDebugEnabled) { + log.debug("RequestedResolved : {}", requestedResolved); + } + + // check snapshot/restore requests + if (snapshotRestoreEvaluator.evaluate(request, task, action0, clusterInfoHolder, presponse).isComplete()) { + return presponse; + } + + // Security index access + if (systemIndexAccessEvaluator.evaluate(request, task, action0, requestedResolved, presponse, context, actionPrivileges, user) + .isComplete()) { + return presponse; + } + + // Protected index access + if (protectedIndexAccessEvaluator.evaluate(request, task, action0, requestedResolved, presponse, mappedRoles).isComplete()) { + return presponse; + } + + // check access for point in time requests + if (pitPrivilegesEvaluator.evaluate(request, context, actionPrivileges, action0, presponse, irr).isComplete()) { + return presponse; + } + + final boolean dnfofEnabled = dcm.isDnfofEnabled(); + + final boolean isTraceEnabled = log.isTraceEnabled(); + if (isTraceEnabled) { + log.trace("dnfof enabled? {}", dnfofEnabled); + } + + final boolean serviceAccountUser = user.isServiceAccount(); + if (isClusterPerm(action0)) { + if (serviceAccountUser) { + log.info("{} is a service account which doesn't have access to cluster level permission: {}", user, action0); + return PrivilegesEvaluatorResponse.insufficient(action0); + } + + presponse = actionPrivileges.hasClusterPrivilege(context, action0); + + if (!presponse.allowed) { + log.info( + "No cluster-level perm match for {} {} [Action [{}]] [RolesChecked {}]. No permissions for {}", + user, + requestedResolved, + action0, + mappedRoles, + presponse.getMissingPrivileges() + ); + return presponse; + } else { + + if (request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { + if (isDebugEnabled) { + log.debug("Normally allowed but we need to apply some extra checks for a restore request."); + } + } else { + if (privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { + + final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( + request, + action0, + user, + dcm, + requestedResolved, + mapTenants(user, mappedRoles) + ); + + if (isDebugEnabled) { + log.debug("Result from privileges interceptor for cluster perm: {}", replaceResult); + } + + if (!replaceResult.continueEvaluation) { + if (replaceResult.accessDenied) { + auditLog.logMissingPrivileges(action0, request, task); + } else { + presponse.allowed = true; + presponse.createIndexRequestBuilder = replaceResult.createIndexRequestBuilder; + } + return presponse; + } + } + + if (isDebugEnabled) { + log.debug("Allowed because we have cluster permissions for {}", action0); + } + presponse.allowed = true; + return presponse; + } + + } + } + + if (checkDocAllowListHeader(user, action0, request)) { + presponse.allowed = true; + return presponse; + } + + // term aggregations + if (termsAggregationEvaluator.evaluate(requestedResolved, request, context, actionPrivileges, presponse).isComplete()) { + return presponse; + } + + ImmutableSet allIndexPermsRequired = evaluateAdditionalIndexPermissions(request, action0); + + if (isDebugEnabled) { + log.debug( + "Requested {} from {}", + allIndexPermsRequired, + threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS) + ); + } + + if (isDebugEnabled) { + log.debug("Requested resolved index types: {}", requestedResolved); + log.debug("Security roles: {}", mappedRoles); + } + + // TODO exclude Security index + + if (privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { + + final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( + request, + action0, + user, + dcm, + requestedResolved, + mapTenants(user, mappedRoles) + ); + + if (isDebugEnabled) { + log.debug("Result from privileges interceptor: {}", replaceResult); + } + + if (!replaceResult.continueEvaluation) { + if (replaceResult.accessDenied) { + auditLog.logMissingPrivileges(action0, request, task); + return PrivilegesEvaluatorResponse.insufficient(action0); + } else { + presponse.allowed = true; + presponse.createIndexRequestBuilder = replaceResult.createIndexRequestBuilder; + return presponse; + } + } + } + + boolean dnfofPossible = dnfofEnabled && DNFOF_MATCHER.test(action0); + + presponse = actionPrivileges.hasIndexPrivilege(context, allIndexPermsRequired, requestedResolved); + + if (presponse.isPartiallyOk()) { + if (dnfofPossible) { + if (irr.replace(request, true, presponse.getAvailableIndices())) { + return PrivilegesEvaluatorResponse.ok(); + } + } + } else if (!presponse.isAllowed()) { + if (dnfofPossible && dcm.isDnfofForEmptyResultsEnabled() && request instanceof IndicesRequest.Replaceable) { + ((IndicesRequest.Replaceable) request).indices(new String[0]); + + if (request instanceof SearchRequest) { + ((SearchRequest) request).indicesOptions(ALLOW_EMPTY); + } else if (request instanceof ClusterSearchShardsRequest) { + ((ClusterSearchShardsRequest) request).indicesOptions(ALLOW_EMPTY); + } else if (request instanceof GetFieldMappingsRequest) { + ((GetFieldMappingsRequest) request).indicesOptions(ALLOW_EMPTY); + } + + return PrivilegesEvaluatorResponse.ok(); + } + } + + if (presponse.isAllowed()) { + if (checkFilteredAliases(requestedResolved, action0, isDebugEnabled)) { + presponse.allowed = false; + return presponse; + } + + if (isDebugEnabled) { + log.debug("Allowed because we have all indices permissions for {}", action0); + } + } else { + log.info( + "No {}-level perm match for {} {}: {} [Action [{}]] [RolesChecked {}]", + "index", + user, + requestedResolved, + presponse.getReason(), + action0, + mappedRoles + ); + log.info("Index to privilege matrix:\n{}", presponse.getPrivilegeMatrix()); + if (presponse.hasEvaluationExceptions()) { + log.info("Evaluation errors:\n{}", presponse.getEvaluationExceptionInfo()); + } + } + + return presponse; + } + + public Set mapRoles(final User user, final TransportAddress caller) { + return this.configModel.mapSecurityRoles(user, caller); + } + + public Map mapTenants(final User user, Set roles) { + return this.configModel.mapTenants(user, roles); + } + + public Set getAllConfiguredTenantNames() { + + return configModel.getAllConfiguredTenantNames(); + } + + public boolean multitenancyEnabled() { + return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDashboardsMultitenancyEnabled(); + } + + public boolean privateTenantEnabled() { + return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDashboardsPrivateTenantEnabled(); + } + + public String dashboardsDefaultTenant() { + return dcm.getDashboardsDefaultTenant(); + } + + public boolean notFailOnForbiddenEnabled() { + return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDnfofEnabled(); + } + + public String dashboardsIndex() { + return dcm.getDashboardsIndexname(); + } + + public String dashboardsServerUsername() { + return dcm.getDashboardsServerUsername(); + } + + public String dashboardsOpenSearchRole() { + return dcm.getDashboardsOpenSearchRole(); + } + + public List getSignInOptions() { + return dcm.getSignInOptions(); + } + + public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege(PrivilegesEvaluationContext context, Set actions, String index) { + return getActionPrivileges().hasExplicitIndexPrivilege(context, actions, Resolved.ofIndex(index)); + } + + private ImmutableSet evaluateAdditionalIndexPermissions(final ActionRequest request, final String originalAction) { + ImmutableSet.Builder additionalPermissionsRequired = ImmutableSet.builder(); + + if (!isClusterPerm(originalAction)) { + additionalPermissionsRequired.add(originalAction); + } + + if (request instanceof ClusterSearchShardsRequest) { + additionalPermissionsRequired.add(SearchAction.NAME); + } + + if (request instanceof BulkShardRequest) { + BulkShardRequest bsr = (BulkShardRequest) request; + for (BulkItemRequest bir : bsr.items()) { + switch (bir.request().opType()) { + case CREATE: + additionalPermissionsRequired.add(IndexAction.NAME); + break; + case INDEX: + additionalPermissionsRequired.add(IndexAction.NAME); + break; + case DELETE: + additionalPermissionsRequired.add(DeleteAction.NAME); + break; + case UPDATE: + additionalPermissionsRequired.add(UpdateAction.NAME); + break; + } + } + } + + if (request instanceof IndicesAliasesRequest) { + IndicesAliasesRequest bsr = (IndicesAliasesRequest) request; + for (AliasActions bir : bsr.getAliasActions()) { + switch (bir.actionType()) { + case REMOVE_INDEX: + additionalPermissionsRequired.add(DeleteIndexAction.NAME); + break; + default: + break; + } + } + } + + if (request instanceof CreateIndexRequest) { + CreateIndexRequest cir = (CreateIndexRequest) request; + if (cir.aliases() != null && !cir.aliases().isEmpty()) { + additionalPermissionsRequired.add(IndicesAliasesAction.NAME); + } + } + + if (request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { + additionalPermissionsRequired.addAll(ConfigConstants.SECURITY_SNAPSHOT_RESTORE_NEEDED_WRITE_PRIVILEGES); + } + + ImmutableSet result = additionalPermissionsRequired.build(); + + if (result.size() > 1) { + traceAction("Additional permissions required: {}", result); + } + + if (log.isDebugEnabled() && result.size() > 1) { + log.debug("Additional permissions required: {}", result); + } + + return result; + } + + public static boolean isClusterPerm(String action0) { + return (action0.startsWith("cluster:") + || action0.startsWith("indices:admin/template/") + || action0.startsWith("indices:admin/index_template/") + || action0.startsWith(SearchScrollAction.NAME) + || (action0.equals(BulkAction.NAME)) + || (action0.equals(MultiGetAction.NAME)) + || (action0.startsWith(MultiSearchAction.NAME)) + || (action0.equals(MultiTermVectorsAction.NAME)) + || (action0.equals(ReindexAction.NAME)) + || (action0.equals(RenderSearchTemplateAction.NAME))); + } + + @SuppressWarnings("unchecked") + private boolean checkFilteredAliases(Resolved requestedResolved, String action, boolean isDebugEnabled) { + final String faMode = dcm.getFilteredAliasMode();// getConfigSettings().dynamic.filtered_alias_mode; + + if (!"disallow".equals(faMode)) { + return false; + } + + if (!ACTION_MATCHER.test(action)) { + return false; + } + + Iterable indexMetaDataCollection; + + if (requestedResolved.isLocalAll()) { + indexMetaDataCollection = new Iterable() { + @Override + public Iterator iterator() { + return clusterStateSupplier.get().getMetadata().getIndices().values().iterator(); + } + }; + } else { + Set indexMetaDataSet = new HashSet<>(requestedResolved.getAllIndices().size()); + + for (String requestAliasOrIndex : requestedResolved.getAllIndices()) { + IndexMetadata indexMetaData = clusterStateSupplier.get().getMetadata().getIndices().get(requestAliasOrIndex); + if (indexMetaData == null) { + if (isDebugEnabled) { + log.debug("{} does not exist in cluster metadata", requestAliasOrIndex); + } + continue; + } + + indexMetaDataSet.add(indexMetaData); + } + + indexMetaDataCollection = indexMetaDataSet; + } + // check filtered aliases + for (IndexMetadata indexMetaData : indexMetaDataCollection) { + + final List filteredAliases = new ArrayList(); + + final Map aliases = indexMetaData.getAliases(); + + if (aliases != null && aliases.size() > 0) { + if (isDebugEnabled) { + log.debug("Aliases for {}: {}", indexMetaData.getIndex().getName(), aliases); + } + + final Iterator it = aliases.keySet().iterator(); + while (it.hasNext()) { + final String alias = it.next(); + final AliasMetadata aliasMetadata = aliases.get(alias); + + if (aliasMetadata != null && aliasMetadata.filteringRequired()) { + filteredAliases.add(aliasMetadata); + if (isDebugEnabled) { + log.debug("{} is a filtered alias {}", alias, aliasMetadata.getFilter()); + } + } else { + if (isDebugEnabled) { + log.debug("{} is not an alias or does not have a filter", alias); + } + } + } + } + + if (filteredAliases.size() > 1 && ACTION_MATCHER.test(action)) { + // TODO add queries as dls queries (works only if dls module is installed) + log.error( + "More than one ({}) filtered alias found for same index ({}). This is currently not supported. Aliases: {}", + filteredAliases.size(), + indexMetaData.getIndex().getName(), + toString(filteredAliases) + ); + return true; + } + } // end-for + + return false; + } + + private boolean checkDocAllowListHeader(User user, String action, ActionRequest request) { + String docAllowListHeader = threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DOC_ALLOWLIST_HEADER); + + if (docAllowListHeader == null) { + return false; + } + + if (!(request instanceof GetRequest)) { + return false; + } + + try { + DocumentAllowList documentAllowList = DocumentAllowList.parse(docAllowListHeader); + GetRequest getRequest = (GetRequest) request; + + if (documentAllowList.isAllowed(getRequest.index(), getRequest.id())) { + if (log.isDebugEnabled()) { + log.debug("Request " + request + " is allowed by " + documentAllowList); + } + + return true; + } else { + return false; + } + + } catch (Exception e) { + log.error("Error while handling document allow list: " + docAllowListHeader, e); + return false; + } + } + + private List toString(List aliases) { + if (aliases == null || aliases.size() == 0) { + return Collections.emptyList(); + } + + final List ret = new ArrayList<>(aliases.size()); + + for (final AliasMetadata amd : aliases) { + if (amd != null) { + ret.add(amd.alias()); + } + } + + return Collections.unmodifiableList(ret); + } +} diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java index 915514264c..79e4c84751 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java @@ -26,33 +26,115 @@ package org.opensearch.security.privileges; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; +import com.google.common.collect.ImmutableSet; + import org.opensearch.action.admin.indices.create.CreateIndexRequestBuilder; +import com.selectivem.collections.CheckTable; + public class PrivilegesEvaluatorResponse { - boolean allowed = false; - Set missingPrivileges = new HashSet(); - Set missingSecurityRoles = new HashSet<>(); - Set resolvedSecurityRoles = new HashSet<>(); + public boolean allowed = false; + public Set missingSecurityRoles = new HashSet<>(); PrivilegesEvaluatorResponseState state = PrivilegesEvaluatorResponseState.PENDING; - CreateIndexRequestBuilder createIndexRequestBuilder; - + public CreateIndexRequestBuilder createIndexRequestBuilder; + private Set onlyAllowedForIndices = ImmutableSet.of(); + private CheckTable indexToActionCheckTable; + private String privilegeMatrix; + private String reason; + + /** + * Contains issues that were encountered during privilege evaluation. Can be used for logging. + */ + private List evaluationExceptions = new ArrayList<>(); + + /** + * Returns true if the request can be fully allowed. See also isAllowedForSpecificIndices(). + */ public boolean isAllowed() { return allowed; } + /** + * Returns true if the request can be allowed if the referenced indices are reduced (aka "do not fail on forbidden"). + * See getAvailableIndices() for the indices for which we have privileges. + */ + public boolean isPartiallyOk() { + return !this.onlyAllowedForIndices.isEmpty(); + } + + /** + * In case isPartiallyOk() is true, this returns the indices for which we have privileges. + */ + public Set getAvailableIndices() { + return this.onlyAllowedForIndices; + } + + /** + * In case isAllowed() is false, this returns the privileges (aka action names) for which we do not have sufficient + * privileges. + */ public Set getMissingPrivileges() { - return new HashSet(missingPrivileges); + return this.indexToActionCheckTable != null ? this.indexToActionCheckTable.getIncompleteColumns() : Collections.emptySet(); } - public Set getMissingSecurityRoles() { - return new HashSet<>(missingSecurityRoles); + /** + * Returns a human-readable reason for the missing privilege. Can be used to make the error message more easy + * to understand. + */ + public String getReason() { + return this.reason; + } + + public PrivilegesEvaluatorResponse reason(String reason) { + this.reason = reason; + return this; + } + + /** + * Returns a diagnostic string that contains issues that were encountered during privilege evaluation. Can be used for logging. + */ + public String getEvaluationExceptionInfo() { + StringBuilder result = new StringBuilder("Exceptions encountered during privilege evaluation:\n"); + + for (PrivilegesEvaluationException evaluationException : this.evaluationExceptions) { + result.append(evaluationException.getNestedMessages()).append("\n"); + } + + return result.toString(); + } + + public boolean hasEvaluationExceptions() { + return !evaluationExceptions.isEmpty(); } - public Set getResolvedSecurityRoles() { - return new HashSet<>(resolvedSecurityRoles); + public PrivilegesEvaluatorResponse evaluationExceptions(Collection evaluationExceptions) { + this.evaluationExceptions.addAll(evaluationExceptions); + return this; + } + + /** + * Returns an ASCII string showing a matrix of available/missing privileges. + * Rows represent indices, columns represent actions. + */ + public String getPrivilegeMatrix() { + String result = this.privilegeMatrix; + + if (result == null) { + result = this.indexToActionCheckTable.toTableString("ok", "MISSING"); + this.privilegeMatrix = result; + } + return result; + } + + public Set getMissingSecurityRoles() { + return new HashSet<>(missingSecurityRoles); } public CreateIndexRequestBuilder getCreateIndexRequestBuilder() { @@ -79,11 +161,52 @@ public boolean isPending() { @Override public String toString() { - return "PrivEvalResponse [allowed=" + allowed + ", missingPrivileges=" + missingPrivileges + "]"; + return "PrivEvalResponse [\nallowed=" + + allowed + + ",\nonlyAllowedForIndices=" + + onlyAllowedForIndices + + ",\n" + + (indexToActionCheckTable != null ? indexToActionCheckTable.toTableString("ok", "MISSING") : "") + + "]"; + } + + public static PrivilegesEvaluatorResponse ok() { + PrivilegesEvaluatorResponse response = new PrivilegesEvaluatorResponse(); + response.allowed = true; + return response; + } + + public static PrivilegesEvaluatorResponse partiallyOk( + Set availableIndices, + CheckTable indexToActionCheckTable + ) { + PrivilegesEvaluatorResponse response = new PrivilegesEvaluatorResponse(); + response.onlyAllowedForIndices = ImmutableSet.copyOf(availableIndices); + response.indexToActionCheckTable = indexToActionCheckTable; + return response; + } + + public static PrivilegesEvaluatorResponse insufficient(String missingPrivilege) { + PrivilegesEvaluatorResponse response = new PrivilegesEvaluatorResponse(); + response.indexToActionCheckTable = CheckTable.create(ImmutableSet.of("_"), ImmutableSet.of(missingPrivilege)); + return response; + } + + public static PrivilegesEvaluatorResponse insufficient(Collection missingPrivileges) { + PrivilegesEvaluatorResponse response = new PrivilegesEvaluatorResponse(); + response.indexToActionCheckTable = CheckTable.create(ImmutableSet.of("_"), ImmutableSet.copyOf(missingPrivileges)); + return response; + } + + public static PrivilegesEvaluatorResponse insufficient(CheckTable indexToActionCheckTable) { + PrivilegesEvaluatorResponse response = new PrivilegesEvaluatorResponse(); + response.indexToActionCheckTable = indexToActionCheckTable; + return response; } public static enum PrivilegesEvaluatorResponseState { PENDING, COMPLETE; } + } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java index f177596573..0e11babc11 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java @@ -42,9 +42,9 @@ public class PrivilegesInterceptor { public static class ReplaceResult { - final boolean continueEvaluation; - final boolean accessDenied; - final CreateIndexRequestBuilder createIndexRequestBuilder; + public final boolean continueEvaluation; + public final boolean accessDenied; + public final CreateIndexRequestBuilder createIndexRequestBuilder; private ReplaceResult(boolean continueEvaluation, boolean accessDenied, CreateIndexRequestBuilder createIndexRequestBuilder) { this.continueEvaluation = continueEvaluation; diff --git a/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java index d1ccb84fc8..8f2788b907 100644 --- a/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java @@ -13,92 +13,8 @@ import java.util.Set; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.OpenSearchSecurityException; -import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.core.common.transport.TransportAddress; -import org.opensearch.security.securityconf.ConfigModel; -import org.opensearch.security.securityconf.SecurityRoles; -import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; -import org.opensearch.threadpool.ThreadPool; - -import org.greenrobot.eventbus.Subscribe; - -public class RestLayerPrivilegesEvaluator { - protected final Logger log = LogManager.getLogger(this.getClass()); - private final ClusterService clusterService; - private ThreadContext threadContext; - private ConfigModel configModel; - - public RestLayerPrivilegesEvaluator(final ClusterService clusterService, final ThreadPool threadPool) { - this.clusterService = clusterService; - this.threadContext = threadPool.getThreadContext(); - } - - @Subscribe - public void onConfigModelChanged(final ConfigModel configModel) { - this.configModel = configModel; - } - - SecurityRoles getSecurityRoles(final Set roles) { - return configModel.getSecurityRoles().filter(roles); - } - - boolean isInitialized() { - return configModel != null && configModel.getSecurityRoles() != null; - } - - public PrivilegesEvaluatorResponse evaluate(final User user, final Set actions) { - if (!isInitialized()) { - throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); - } - - final PrivilegesEvaluatorResponse presponse = new PrivilegesEvaluatorResponse(); - - final TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); - - final Set mappedRoles = mapRoles(user, caller); - - presponse.resolvedSecurityRoles.addAll(mappedRoles); - final SecurityRoles securityRoles = getSecurityRoles(mappedRoles); - - final boolean isDebugEnabled = log.isDebugEnabled(); - if (isDebugEnabled) { - log.debug("Evaluate permissions for {} on {}", user, clusterService.localNode().getName()); - log.debug("Action: {}", actions); - log.debug("Mapped roles: {}", mappedRoles.toString()); - } - - for (final String action : actions) { - if (!securityRoles.impliesClusterPermissionPermission(action)) { - presponse.missingPrivileges.add(action); - presponse.allowed = false; - log.info( - "No permission match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}", - user, - action, - securityRoles.getRoleNames(), - presponse.missingPrivileges - ); - } else { - if (isDebugEnabled) { - log.debug("Allowed because we have permissions for {}", actions); - } - presponse.allowed = true; - - // break the loop as we found the matching permission - break; - } - } - - return presponse; - } - Set mapRoles(final User user, final TransportAddress caller) { - return this.configModel.mapSecurityRoles(user, caller); - } +public interface RestLayerPrivilegesEvaluator { + PrivilegesEvaluatorResponse evaluate(final User user, final String routeName, final Set actions); } diff --git a/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorImpl.java b/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorImpl.java new file mode 100644 index 0000000000..0668eab917 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorImpl.java @@ -0,0 +1,42 @@ +package org.opensearch.security.privileges; + +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.security.user.User; + +public class RestLayerPrivilegesEvaluatorImpl implements RestLayerPrivilegesEvaluator { + protected final Logger log = LogManager.getLogger(this.getClass()); + private final PrivilegesEvaluatorImpl privilegesEvaluator; + + public RestLayerPrivilegesEvaluatorImpl(PrivilegesEvaluatorImpl privilegesEvaluator) { + this.privilegesEvaluator = privilegesEvaluator; + } + + public PrivilegesEvaluatorResponse evaluate(final User user, final String routeName, final Set actions) { + PrivilegesEvaluationContext context = privilegesEvaluator.createContext(user, routeName); + + final boolean isDebugEnabled = log.isDebugEnabled(); + if (isDebugEnabled) { + log.debug("Evaluate permissions for {}", user); + log.debug("Action: {}", actions); + log.debug("Mapped roles: {}", context.getMappedRoles().toString()); + } + + PrivilegesEvaluatorResponse result = privilegesEvaluator.getActionPrivileges().hasAnyClusterPrivilege(context, actions); + + if (!result.allowed) { + log.info( + "No permission match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}", + user, + routeName, + context.getMappedRoles(), + result.getMissingPrivileges() + ); + } + + return result; + } +} diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java index 38825a9bf1..99828f7b17 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java @@ -32,20 +32,18 @@ import java.util.Set; import java.util.stream.Collectors; +import com.google.common.collect.ImmutableSet; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.action.ActionRequest; import org.opensearch.action.RealtimeRequest; import org.opensearch.action.search.SearchRequest; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; import org.opensearch.indices.SystemIndexRegistry; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; -import org.opensearch.security.securityconf.SecurityRoles; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; @@ -72,6 +70,7 @@ public class SystemIndexAccessEvaluator { private final boolean isSystemIndexEnabled; private final boolean isSystemIndexPermissionEnabled; + private final static ImmutableSet SYSTEM_INDEX_PERMISSION_SET = ImmutableSet.of(ConfigConstants.SYSTEM_INDEX_PERMISSION); public SystemIndexAccessEvaluator(final Settings settings, AuditLog auditLog, IndexResolverReplacer irr) { this.securityIndex = settings.get( @@ -128,12 +127,11 @@ public PrivilegesEvaluatorResponse evaluate( final String action, final Resolved requestedResolved, final PrivilegesEvaluatorResponse presponse, - final SecurityRoles securityRoles, - final User user, - final IndexNameExpressionResolver resolver, - final ClusterService clusterService + final PrivilegesEvaluationContext context, + final ActionPrivileges actionPrivileges, + final User user ) { - evaluateSystemIndicesAccess(action, requestedResolved, request, task, presponse, securityRoles, user, resolver, clusterService); + evaluateSystemIndicesAccess(action, requestedResolved, request, task, presponse, context, actionPrivileges, user); if (requestedResolved.isLocalAll() || requestedResolved.getAllIndices().contains(securityIndex) @@ -235,10 +233,9 @@ private boolean isActionAllowed(String action) { * @param request the action request to be used for audit logging * @param task task in which this access check will be performed * @param presponse the pre-response object that will eventually become a response and returned to the requester - * @param securityRoles user's roles which will be used for access evaluation + * @param context conveys information about user and mapped roles, etc. + * @param actionPrivileges the up-to-date ActionPrivileges instance * @param user this user's permissions will be looked up - * @param resolver the index expression resolver - * @param clusterService required to fetch cluster state metadata */ private void evaluateSystemIndicesAccess( final String action, @@ -246,10 +243,9 @@ private void evaluateSystemIndicesAccess( final ActionRequest request, final Task task, final PrivilegesEvaluatorResponse presponse, - SecurityRoles securityRoles, - final User user, - final IndexNameExpressionResolver resolver, - final ClusterService clusterService + final PrivilegesEvaluationContext context, + final ActionPrivileges actionPrivileges, + final User user ) { // Perform access check is system index permissions are enabled boolean containsSystemIndex = requestContainsAnySystemIndices(requestedResolved); @@ -260,7 +256,7 @@ private void evaluateSystemIndicesAccess( if (serviceAccountUser && containsRegularIndex) { auditLog.logSecurityIndexAttempt(request, action, task); if (!containsSystemIndex && log.isInfoEnabled()) { - log.info("{} not permitted for a service account {} on non-system indices.", action, securityRoles); + log.info("{} not permitted for a service account {} on non-system indices.", action, context.getMappedRoles()); } else if (containsSystemIndex && log.isDebugEnabled()) { List regularIndices = requestedResolved.getAllIndices() .stream() @@ -282,7 +278,7 @@ private void evaluateSystemIndicesAccess( log.info( "{} not permitted for a regular user {} on protected system indices {}", action, - securityRoles, + context.getMappedRoles(), String.join(", ", getAllProtectedSystemIndices(requestedResolved)) ); } @@ -290,19 +286,13 @@ private void evaluateSystemIndicesAccess( presponse.markComplete(); return; } else if (containsSystemIndex - && !securityRoles.hasExplicitIndexPermission( - requestedResolved, - user, - new String[] { ConfigConstants.SYSTEM_INDEX_PERMISSION }, - resolver, - clusterService - )) { + && !actionPrivileges.hasExplicitIndexPrivilege(context, SYSTEM_INDEX_PERMISSION_SET, requestedResolved).isAllowed()) { auditLog.logSecurityIndexAttempt(request, action, task); if (log.isInfoEnabled()) { log.info( "No {} permission for user roles {} to System Indices {}", action, - securityRoles, + context.getMappedRoles(), String.join(", ", getAllSystemIndices(requestedResolved)) ); } diff --git a/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java b/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java index cc0bf25b5e..a2cd1c16a7 100644 --- a/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java @@ -26,8 +26,8 @@ package org.opensearch.security.privileges; -import java.util.Set; - +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -38,27 +38,24 @@ import org.opensearch.action.search.MultiSearchAction; import org.opensearch.action.search.SearchAction; import org.opensearch.action.search.SearchRequest; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.service.ClusterService; import org.opensearch.index.query.MatchNoneQueryBuilder; import org.opensearch.index.query.QueryBuilder; import org.opensearch.index.query.TermsQueryBuilder; import org.opensearch.search.aggregations.AggregationBuilder; import org.opensearch.search.aggregations.bucket.terms.TermsAggregationBuilder; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; -import org.opensearch.security.securityconf.SecurityRoles; -import org.opensearch.security.user.User; public class TermsAggregationEvaluator { protected final Logger log = LogManager.getLogger(this.getClass()); - private static final String[] READ_ACTIONS = new String[] { + private static final ImmutableSet READ_ACTIONS = ImmutableSet.of( MultiSearchAction.NAME, MultiGetAction.NAME, GetAction.NAME, SearchAction.NAME, - FieldCapabilitiesAction.NAME }; + FieldCapabilitiesAction.NAME + ); private static final QueryBuilder NONE_QUERY = new MatchNoneQueryBuilder(); @@ -67,10 +64,8 @@ public TermsAggregationEvaluator() {} public PrivilegesEvaluatorResponse evaluate( final Resolved resolved, final ActionRequest request, - ClusterService clusterService, - User user, - SecurityRoles securityRoles, - IndexNameExpressionResolver resolver, + PrivilegesEvaluationContext context, + ActionPrivileges actionPrivileges, PrivilegesEvaluatorResponse presponse ) { try { @@ -89,17 +84,22 @@ public PrivilegesEvaluatorResponse evaluate( && ab.getPipelineAggregations().isEmpty() && ab.getSubAggregations().isEmpty()) { - final Set allPermittedIndices = securityRoles.getAllPermittedIndicesForDashboards( - resolved, - user, + PrivilegesEvaluatorResponse subResponse = actionPrivileges.hasIndexPrivilege( + context, READ_ACTIONS, - resolver, - clusterService + Resolved._LOCAL_ALL ); - if (allPermittedIndices == null || allPermittedIndices.isEmpty()) { + + if (subResponse.isPartiallyOk()) { + sr.source() + .query( + new TermsQueryBuilder( + "_index", + Sets.union(subResponse.getAvailableIndices(), resolved.getRemoteIndices()) + ) + ); + } else if (!subResponse.isAllowed()) { sr.source().query(NONE_QUERY); - } else { - sr.source().query(new TermsQueryBuilder("_index", allPermittedIndices)); } presponse.allowed = true; diff --git a/src/main/java/org/opensearch/security/privileges/UserAttributes.java b/src/main/java/org/opensearch/security/privileges/UserAttributes.java index e138c5f621..88a858789c 100644 --- a/src/main/java/org/opensearch/security/privileges/UserAttributes.java +++ b/src/main/java/org/opensearch/security/privileges/UserAttributes.java @@ -15,6 +15,7 @@ import com.google.common.base.Joiner; import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; import org.opensearch.security.user.User; @@ -24,6 +25,34 @@ * This code was moved over from ConfigModelV7. */ public class UserAttributes { + public static String replaceProperties(String orig, PrivilegesEvaluationContext context) { + User user = context.getUser(); + + orig = orig.replace("${user.name}", user.getName()).replace("${user_name}", user.getName()); + orig = replaceRoles(orig, user); + orig = replaceSecurityRoles(orig, context); + for (Map.Entry entry : user.getCustomAttributesMap().entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) { + continue; + } + orig = orig.replace("${" + entry.getKey() + "}", entry.getValue()); + orig = orig.replace("${" + entry.getKey().replace('.', '_') + "}", entry.getValue()); + } + return orig; + } + + private static String replaceSecurityRoles(final String orig, PrivilegesEvaluationContext context) { + String retVal = orig; + if (orig.contains("${user.securityRoles}") || orig.contains("${user_securityRoles}")) { + final String commaSeparatedRoles = toQuotedCommaSeparatedString( + Sets.union(context.getUser().getSecurityRoles(), context.getMappedRoles()) + ); + retVal = orig.replace("${user.securityRoles}", commaSeparatedRoles).replace("${user_securityRoles}", commaSeparatedRoles); + } + return retVal; + } + + @Deprecated public static String replaceProperties(String orig, User user) { if (user == null || orig == null) { diff --git a/src/main/java/org/opensearch/security/privileges/WellKnownActions.java b/src/main/java/org/opensearch/security/privileges/WellKnownActions.java new file mode 100644 index 0000000000..af4f0bb025 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/WellKnownActions.java @@ -0,0 +1,88 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.security.privileges; + +import com.google.common.collect.ImmutableSet; + +import org.opensearch.action.admin.cluster.health.ClusterHealthAction; +import org.opensearch.action.admin.cluster.node.stats.NodesStatsAction; +import org.opensearch.action.admin.cluster.state.ClusterStateAction; +import org.opensearch.action.admin.cluster.stats.ClusterStatsAction; +import org.opensearch.action.admin.indices.analyze.AnalyzeAction; +import org.opensearch.action.admin.indices.create.AutoCreateAction; +import org.opensearch.action.admin.indices.mapping.put.AutoPutMappingAction; +import org.opensearch.action.admin.indices.mapping.put.PutMappingAction; +import org.opensearch.action.admin.indices.refresh.RefreshAction; +import org.opensearch.action.admin.indices.refresh.TransportShardRefreshAction; +import org.opensearch.action.bulk.BulkAction; +import org.opensearch.action.bulk.TransportShardBulkAction; +import org.opensearch.action.delete.DeleteAction; +import org.opensearch.action.fieldcaps.FieldCapabilitiesAction; +import org.opensearch.action.get.GetAction; +import org.opensearch.action.get.MultiGetAction; +import org.opensearch.action.index.IndexAction; +import org.opensearch.action.main.MainAction; +import org.opensearch.action.search.ClearScrollAction; +import org.opensearch.action.search.MultiSearchAction; +import org.opensearch.action.search.SearchAction; +import org.opensearch.action.search.SearchScrollAction; +import org.opensearch.action.termvectors.MultiTermVectorsAction; +import org.opensearch.action.termvectors.TermVectorsAction; +import org.opensearch.action.update.UpdateAction; +import org.opensearch.index.reindex.DeleteByQueryAction; +import org.opensearch.index.reindex.UpdateByQueryAction; +import org.opensearch.security.support.ConfigConstants; + +/** + * This class lists so-called "well-known actions". These are taken into account when creating the pre-computed + * data structures of the ActionPrivileges class. Thus, a very fast performance evaluation will be possible for + * these actions. The trade-off is that each well-known action increases the heap footprint required by the data + * structures. Thus, it makes sense to limit these actions to these which are really performance critical. + */ +public class WellKnownActions { + public static final ImmutableSet CLUSTER_ACTIONS = ImmutableSet.of( + MultiGetAction.NAME, + BulkAction.NAME, + SearchScrollAction.NAME, + MultiSearchAction.NAME, + MultiTermVectorsAction.NAME, + ClearScrollAction.NAME, + MainAction.NAME, + ClusterStatsAction.NAME, + ClusterStateAction.NAME, + ClusterHealthAction.NAME, + NodesStatsAction.NAME + ); + + public static final ImmutableSet INDEX_ACTIONS = ImmutableSet.of( + IndexAction.NAME, + GetAction.NAME, + TermVectorsAction.NAME, + DeleteAction.NAME, + UpdateAction.NAME, + SearchAction.NAME, + UpdateByQueryAction.NAME, + DeleteByQueryAction.NAME, + TransportShardBulkAction.ACTION_NAME, + PutMappingAction.NAME, + AutoPutMappingAction.NAME, + AnalyzeAction.NAME, + AutoCreateAction.NAME, + RefreshAction.NAME, + TransportShardRefreshAction.NAME, + FieldCapabilitiesAction.NAME + ); + + /** + * Compare https://github.com/opensearch-project/security/pull/2887 + */ + public static final ImmutableSet EXPLICITLY_REQUIRED_INDEX_ACTIONS = ImmutableSet.of(ConfigConstants.SYSTEM_INDEX_PERMISSION); +} diff --git a/src/main/java/org/opensearch/security/privileges/legacy/PitPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/legacy/PitPrivilegesEvaluator.java new file mode 100644 index 0000000000..dde32b4b19 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/legacy/PitPrivilegesEvaluator.java @@ -0,0 +1,117 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.security.privileges.legacy; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.admin.indices.segments.PitSegmentsRequest; +import org.opensearch.action.search.CreatePitRequest; +import org.opensearch.action.search.DeletePitRequest; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.unit.TimeValue; +import org.opensearch.security.OpenSearchSecurityPlugin; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.securityconf.SecurityRoles; +import org.opensearch.security.user.User; + +/** + * This class evaluates privileges for point in time (Delete and List all) operations. + * For aliases - users must have either alias permission or backing index permissions + * For data streams - users must have access to backing indices permission + data streams permission. + */ +public class PitPrivilegesEvaluator { + + public PrivilegesEvaluatorResponse evaluate( + final ActionRequest request, + final ClusterService clusterService, + final User user, + final SecurityRoles securityRoles, + final String action, + final IndexNameExpressionResolver resolver, + final PrivilegesEvaluatorResponse presponse, + final IndexResolverReplacer irr + ) { + + if (!(request instanceof DeletePitRequest || request instanceof PitSegmentsRequest)) { + return presponse; + } + List pitIds = new ArrayList<>(); + + if (request instanceof DeletePitRequest) { + DeletePitRequest deletePitRequest = (DeletePitRequest) request; + pitIds = deletePitRequest.getPitIds(); + } else if (request instanceof PitSegmentsRequest) { + PitSegmentsRequest pitSegmentsRequest = (PitSegmentsRequest) request; + pitIds = pitSegmentsRequest.getPitIds(); + } + // if request is for all PIT IDs, skip custom pit ids evaluation + if (pitIds.size() == 1 && "_all".equals(pitIds.get(0))) { + return presponse; + } else { + return handlePitsAccess(pitIds, clusterService, user, securityRoles, action, resolver, presponse, irr); + } + } + + /** + * Handle access for delete operation / pit segments operation where PIT IDs are explicitly passed + */ + private PrivilegesEvaluatorResponse handlePitsAccess( + List pitIds, + ClusterService clusterService, + User user, + SecurityRoles securityRoles, + final String action, + IndexNameExpressionResolver resolver, + PrivilegesEvaluatorResponse presponse, + final IndexResolverReplacer irr + ) { + Map pitToIndicesMap = OpenSearchSecurityPlugin.GuiceHolder.getPitService().getIndicesForPits(pitIds); + Set pitIndices = new HashSet<>(); + // add indices across all PITs to a set and evaluate if user has access to all indices + for (String[] indices : pitToIndicesMap.values()) { + pitIndices.addAll(Arrays.asList(indices)); + } + Set allPermittedIndices = getPermittedIndices(pitIndices, clusterService, user, securityRoles, action, resolver, irr); + // Only if user has access to all PIT's indices, allow operation, otherwise continue evaluation in PrivilegesEvaluator. + if (allPermittedIndices.containsAll(pitIndices)) { + presponse.allowed = true; + presponse.markComplete(); + } + return presponse; + } + + /** + * This method returns list of permitted indices for the PIT indices passed + */ + private Set getPermittedIndices( + Set pitIndices, + ClusterService clusterService, + User user, + SecurityRoles securityRoles, + final String action, + IndexNameExpressionResolver resolver, + final IndexResolverReplacer irr + ) { + String[] indicesArr = new String[pitIndices.size()]; + CreatePitRequest req = new CreatePitRequest(new TimeValue(1, TimeUnit.DAYS), true, pitIndices.toArray(indicesArr)); + final IndexResolverReplacer.Resolved pitResolved = irr.resolveRequest(req); + return securityRoles.reduce(pitResolved, user, new String[] { action }, resolver, clusterService); + } +} diff --git a/src/main/java/org/opensearch/security/privileges/legacy/PrivilegesEvaluatorImpl.java b/src/main/java/org/opensearch/security/privileges/legacy/PrivilegesEvaluatorImpl.java new file mode 100644 index 0000000000..75f191eba7 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/legacy/PrivilegesEvaluatorImpl.java @@ -0,0 +1,821 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.privileges.legacy; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.action.ActionRequest; +import org.opensearch.action.IndicesRequest; +import org.opensearch.action.admin.cluster.shards.ClusterSearchShardsRequest; +import org.opensearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; +import org.opensearch.action.admin.indices.alias.IndicesAliasesAction; +import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; +import org.opensearch.action.admin.indices.create.AutoCreateAction; +import org.opensearch.action.admin.indices.create.CreateIndexAction; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.admin.indices.delete.DeleteIndexAction; +import org.opensearch.action.admin.indices.mapping.get.GetFieldMappingsRequest; +import org.opensearch.action.admin.indices.mapping.put.AutoPutMappingAction; +import org.opensearch.action.admin.indices.mapping.put.PutMappingAction; +import org.opensearch.action.bulk.BulkAction; +import org.opensearch.action.bulk.BulkItemRequest; +import org.opensearch.action.bulk.BulkRequest; +import org.opensearch.action.bulk.BulkShardRequest; +import org.opensearch.action.delete.DeleteAction; +import org.opensearch.action.get.GetRequest; +import org.opensearch.action.get.MultiGetAction; +import org.opensearch.action.index.IndexAction; +import org.opensearch.action.search.MultiSearchAction; +import org.opensearch.action.search.SearchAction; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchScrollAction; +import org.opensearch.action.support.IndicesOptions; +import org.opensearch.action.termvectors.MultiTermVectorsAction; +import org.opensearch.action.update.UpdateAction; +import org.opensearch.cluster.metadata.AliasMetadata; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.common.Strings; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.index.reindex.ReindexAction; +import org.opensearch.script.mustache.RenderSearchTemplateAction; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.configuration.ClusterInfoHolder; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.privileges.DocumentAllowList; +import org.opensearch.security.privileges.PrivilegesEvaluationContext; +import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.privileges.PrivilegesInterceptor; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; +import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.securityconf.DynamicConfigModel; +import org.opensearch.security.securityconf.SecurityRoles; +import org.opensearch.security.securityconf.impl.DashboardSignInOption; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.security.user.User; +import org.opensearch.tasks.Task; +import org.opensearch.threadpool.ThreadPool; + +import org.greenrobot.eventbus.Subscribe; + +import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction; +import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT; + +public class PrivilegesEvaluatorImpl implements PrivilegesEvaluator { + + static final WildcardMatcher DNFOF_MATCHER = WildcardMatcher.from( + ImmutableList.of( + "indices:data/read/*", + "indices:admin/mappings/fields/get*", + "indices:admin/shards/search_shards", + "indices:admin/resolve/index", + "indices:monitor/settings/get", + "indices:monitor/stats", + "indices:admin/aliases/get" + ) + ); + + private static final WildcardMatcher ACTION_MATCHER = WildcardMatcher.from("indices:data/read/*search*"); + + private static final IndicesOptions ALLOW_EMPTY = IndicesOptions.fromOptions(true, true, false, false); + + protected final Logger log = LogManager.getLogger(this.getClass()); + private final ClusterService clusterService; + + private final IndexNameExpressionResolver resolver; + + private final AuditLog auditLog; + private ThreadContext threadContext; + + private PrivilegesInterceptor privilegesInterceptor; + + private final boolean checkSnapshotRestoreWritePrivileges; + + private final ClusterInfoHolder clusterInfoHolder; + private ConfigModel configModel; + private final IndexResolverReplacer irr; + private final SnapshotRestoreEvaluator snapshotRestoreEvaluator; + private final SystemIndexAccessEvaluator systemIndexAccessEvaluator; + private final ProtectedIndexAccessEvaluator protectedIndexAccessEvaluator; + private final TermsAggregationEvaluator termsAggregationEvaluator; + private final PitPrivilegesEvaluator pitPrivilegesEvaluator; + private DynamicConfigModel dcm; + private final NamedXContentRegistry namedXContentRegistry; + + public PrivilegesEvaluatorImpl( + final ClusterService clusterService, + final ThreadPool threadPool, + final ConfigurationRepository configurationRepository, + final IndexNameExpressionResolver resolver, + AuditLog auditLog, + final Settings settings, + final PrivilegesInterceptor privilegesInterceptor, + final ClusterInfoHolder clusterInfoHolder, + final IndexResolverReplacer irr, + NamedXContentRegistry namedXContentRegistry + ) { + + super(); + this.clusterService = clusterService; + this.resolver = resolver; + this.auditLog = auditLog; + + this.threadContext = threadPool.getThreadContext(); + this.privilegesInterceptor = privilegesInterceptor; + + this.checkSnapshotRestoreWritePrivileges = settings.getAsBoolean( + ConfigConstants.SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, + ConfigConstants.SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES + ); + + this.clusterInfoHolder = clusterInfoHolder; + this.irr = irr; + snapshotRestoreEvaluator = new SnapshotRestoreEvaluator(settings, auditLog); + systemIndexAccessEvaluator = new SystemIndexAccessEvaluator(settings, auditLog, irr); + protectedIndexAccessEvaluator = new ProtectedIndexAccessEvaluator(settings, auditLog); + termsAggregationEvaluator = new TermsAggregationEvaluator(); + pitPrivilegesEvaluator = new PitPrivilegesEvaluator(); + this.namedXContentRegistry = namedXContentRegistry; + } + + @Subscribe + public void onConfigModelChanged(ConfigModel configModel) { + this.configModel = configModel; + } + + @Subscribe + public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { + this.dcm = dcm; + } + + public SecurityRoles getSecurityRoles(Set roles) { + return configModel.getSecurityRoles().filter(roles); + } + + public boolean hasRestAdminPermissions(final User user, final TransportAddress remoteAddress, final String permissions) { + final Set userRoles = mapRoles(user, remoteAddress); + return hasRestAdminPermissions(userRoles, permissions); + } + + private boolean hasRestAdminPermissions(final Set roles, String permission) { + final SecurityRoles securityRoles = getSecurityRoles(roles); + return securityRoles.hasExplicitClusterPermissionPermission(permission); + } + + public boolean isInitialized() { + return configModel != null && configModel.getSecurityRoles() != null && dcm != null; + } + + private void setUserInfoInThreadContext(User user) { + if (threadContext.getTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT) == null) { + StringJoiner joiner = new StringJoiner("|"); + joiner.add(user.getName()); + joiner.add(String.join(",", user.getRoles())); + joiner.add(String.join(",", user.getSecurityRoles())); + String requestedTenant = user.getRequestedTenant(); + if (!Strings.isNullOrEmpty(requestedTenant)) { + joiner.add(requestedTenant); + } + threadContext.putTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT, joiner.toString()); + } + } + + public PrivilegesEvaluationContext createContext(User user, String action) { + return createContext(user, action, null, null, null); + } + + public PrivilegesEvaluationContext createContext( + User user, + String action0, + ActionRequest request, + Task task, + Set injectedRoles + ) { + if (!isInitialized()) { + throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); + } + + TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + ImmutableSet mappedRoles = ImmutableSet.copyOf((injectedRoles == null) ? mapRoles(user, caller) : injectedRoles); + + return new PrivilegesEvaluationContext(user, mappedRoles, action0, request, task, irr, resolver, () -> clusterService.state()); + } + + public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { + + if (!isInitialized()) { + throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); + } + + String action0 = context.getAction(); + ImmutableSet mappedRoles = context.getMappedRoles(); + User user = context.getUser(); + ActionRequest request = context.getRequest(); + Task task = context.getTask(); + + if (action0.startsWith("internal:indices/admin/upgrade")) { + action0 = "indices:admin/upgrade"; + } + + if (AutoCreateAction.NAME.equals(action0)) { + action0 = CreateIndexAction.NAME; + } + + if (AutoPutMappingAction.NAME.equals(action0)) { + action0 = PutMappingAction.NAME; + } + + final PrivilegesEvaluatorResponse presponse = new PrivilegesEvaluatorResponse(); + + final String injectedRolesValidationString = threadContext.getTransient( + ConfigConstants.OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION + ); + if (injectedRolesValidationString != null) { + HashSet injectedRolesValidationSet = new HashSet<>(Arrays.asList(injectedRolesValidationString.split(","))); + if (!mappedRoles.containsAll(injectedRolesValidationSet)) { + presponse.allowed = false; + presponse.missingSecurityRoles.addAll(injectedRolesValidationSet); + log.info("Roles {} are not mapped to the user {}", injectedRolesValidationSet, user); + return presponse; + } + mappedRoles = ImmutableSet.copyOf(injectedRolesValidationSet); + context.setMappedRoles(mappedRoles); + } + final SecurityRoles securityRoles = getSecurityRoles(mappedRoles); + + // Add the security roles for this user so that they can be used for DLS parameter substitution. + user.addSecurityRoles(mappedRoles); + setUserInfoInThreadContext(user); + + final boolean isDebugEnabled = log.isDebugEnabled(); + if (isDebugEnabled) { + log.debug("Evaluate permissions for {} on {}", user, clusterService.localNode().getName()); + log.debug("Action: {} ({})", action0, request.getClass().getSimpleName()); + log.debug("Mapped roles: {}", mappedRoles.toString()); + } + + if (request instanceof BulkRequest && (Strings.isNullOrEmpty(user.getRequestedTenant()))) { + // Shortcut for bulk actions. The details are checked on the lower level of the BulkShardRequests (Action + // indices:data/write/bulk[s]). + // This shortcut is only possible if the default tenant is selected, as we might need to rewrite the request for non-default + // tenants. + // No further access check for the default tenant is necessary, as access will be also checked on the TransportShardBulkAction + // level. + + if (!securityRoles.impliesClusterPermissionPermission(action0)) { + log.info( + "No cluster-level perm match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}", + user, + action0, + mappedRoles, + action0 + ); + return PrivilegesEvaluatorResponse.insufficient(action0); + } else { + presponse.allowed = true; + } + return presponse; + } + + final Resolved requestedResolved = context.getResolvedRequest(); + + if (isDebugEnabled) { + log.debug("RequestedResolved : {}", requestedResolved); + } + + // check snapshot/restore requests + if (snapshotRestoreEvaluator.evaluate(request, task, action0, clusterInfoHolder, presponse).isComplete()) { + return presponse; + } + + // Security index access + if (systemIndexAccessEvaluator.evaluate( + request, + task, + action0, + requestedResolved, + presponse, + securityRoles, + user, + resolver, + clusterService + ).isComplete()) { + return presponse; + } + + // Protected index access + if (protectedIndexAccessEvaluator.evaluate(request, task, action0, requestedResolved, presponse, mappedRoles).isComplete()) { + return presponse; + } + + // check access for point in time requests + if (pitPrivilegesEvaluator.evaluate(request, clusterService, user, securityRoles, action0, resolver, presponse, irr).isComplete()) { + return presponse; + } + + final boolean dnfofEnabled = dcm.isDnfofEnabled(); + + final boolean isTraceEnabled = log.isTraceEnabled(); + if (isTraceEnabled) { + log.trace("dnfof enabled? {}", dnfofEnabled); + } + + final boolean serviceAccountUser = user.isServiceAccount(); + if (isClusterPerm(action0)) { + if (serviceAccountUser) { + log.info("{} is a service account which doesn't have access to cluster level permission: {}", user, action0); + return PrivilegesEvaluatorResponse.insufficient(action0); + } + + if (!securityRoles.impliesClusterPermissionPermission(action0)) { + log.info( + "No cluster-level perm match for {} {} [Action [{}]] [RolesChecked {}]. No permissions for {}", + user, + requestedResolved, + action0, + mappedRoles, + action0 + ); + return PrivilegesEvaluatorResponse.insufficient(action0); + } else { + + if (request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { + if (isDebugEnabled) { + log.debug("Normally allowed but we need to apply some extra checks for a restore request."); + } + } else { + if (privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { + + final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( + request, + action0, + user, + dcm, + requestedResolved, + mapTenants(user, mappedRoles) + ); + + if (isDebugEnabled) { + log.debug("Result from privileges interceptor for cluster perm: {}", replaceResult); + } + + if (!replaceResult.continueEvaluation) { + if (replaceResult.accessDenied) { + auditLog.logMissingPrivileges(action0, request, task); + } else { + presponse.allowed = true; + presponse.createIndexRequestBuilder = replaceResult.createIndexRequestBuilder; + } + return presponse; + } + } + + if (isDebugEnabled) { + log.debug("Allowed because we have cluster permissions for {}", action0); + } + presponse.allowed = true; + return presponse; + } + + } + } + + if (checkDocAllowListHeader(user, action0, request)) { + presponse.allowed = true; + return presponse; + } + + // term aggregations + if (termsAggregationEvaluator.evaluate(requestedResolved, request, clusterService, user, securityRoles, resolver, presponse) + .isComplete()) { + return presponse; + } + + final Set allIndexPermsRequired = evaluateAdditionalIndexPermissions(request, action0); + final String[] allIndexPermsRequiredA = allIndexPermsRequired.toArray(new String[0]); + + if (isDebugEnabled) { + log.debug( + "Requested {} from {}", + allIndexPermsRequired, + threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS) + ); + } + + if (isDebugEnabled) { + log.debug("Requested resolved index types: {}", requestedResolved); + log.debug("Security roles: {}", mappedRoles); + } + + // TODO exclude Security index + + if (privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { + + final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( + request, + action0, + user, + dcm, + requestedResolved, + mapTenants(user, mappedRoles) + ); + + if (isDebugEnabled) { + log.debug("Result from privileges interceptor: {}", replaceResult); + } + + if (!replaceResult.continueEvaluation) { + if (replaceResult.accessDenied) { + auditLog.logMissingPrivileges(action0, request, task); + } else { + presponse.allowed = true; + presponse.createIndexRequestBuilder = replaceResult.createIndexRequestBuilder; + } + return presponse; + } + } + + if (dnfofEnabled && DNFOF_MATCHER.test(action0)) { + + if (requestedResolved.getAllIndices().isEmpty()) { + return PrivilegesEvaluatorResponse.ok(); + } + + Set reduced = securityRoles.reduce(requestedResolved, user, allIndexPermsRequiredA, resolver, clusterService); + + if (reduced.isEmpty()) { + if (dcm.isDnfofForEmptyResultsEnabled() && request instanceof IndicesRequest.Replaceable) { + + ((IndicesRequest.Replaceable) request).indices(new String[0]); + + if (request instanceof SearchRequest) { + ((SearchRequest) request).indicesOptions(ALLOW_EMPTY); + } else if (request instanceof ClusterSearchShardsRequest) { + ((ClusterSearchShardsRequest) request).indicesOptions(ALLOW_EMPTY); + } else if (request instanceof GetFieldMappingsRequest) { + ((GetFieldMappingsRequest) request).indicesOptions(ALLOW_EMPTY); + } + + return PrivilegesEvaluatorResponse.ok(); + } + return PrivilegesEvaluatorResponse.insufficient(allIndexPermsRequired); + } + + if (irr.replace(request, true, reduced.toArray(new String[0]))) { + return PrivilegesEvaluatorResponse.ok(); + } + } + + // not bulk, mget, etc request here + boolean permGiven = false; + + if (isDebugEnabled) { + log.debug("Security roles: {}", securityRoles.getRoleNames()); + } + + if (dcm.isMultiRolespanEnabled()) { + permGiven = securityRoles.impliesTypePermGlobal(requestedResolved, user, allIndexPermsRequiredA, resolver, clusterService); + } else { + permGiven = securityRoles.get(requestedResolved, user, allIndexPermsRequiredA, resolver, clusterService); + + } + + if (!permGiven) { + log.info( + "No {}-level perm match for {} {} [Action [{}]] [RolesChecked {}]", + "index", + user, + requestedResolved, + action0, + mappedRoles + ); + log.info("No permissions for {}", allIndexPermsRequired); + } else { + + if (checkFilteredAliases(requestedResolved, action0, isDebugEnabled)) { + presponse.allowed = false; + return presponse; + } + + if (isDebugEnabled) { + log.debug("Allowed because we have all indices permissions for {}", action0); + } + } + + if (permGiven) { + return PrivilegesEvaluatorResponse.ok(); + } else { + return PrivilegesEvaluatorResponse.insufficient(allIndexPermsRequired); + } + } + + public Set mapRoles(final User user, final TransportAddress caller) { + return this.configModel.mapSecurityRoles(user, caller); + } + + public Map mapTenants(final User user, Set roles) { + return this.configModel.mapTenants(user, roles); + } + + public Set getAllConfiguredTenantNames() { + + return configModel.getAllConfiguredTenantNames(); + } + + public boolean multitenancyEnabled() { + return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDashboardsMultitenancyEnabled(); + } + + public boolean privateTenantEnabled() { + return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDashboardsPrivateTenantEnabled(); + } + + public String dashboardsDefaultTenant() { + return dcm.getDashboardsDefaultTenant(); + } + + public boolean notFailOnForbiddenEnabled() { + return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDnfofEnabled(); + } + + public String dashboardsIndex() { + return dcm.getDashboardsIndexname(); + } + + public String dashboardsServerUsername() { + return dcm.getDashboardsServerUsername(); + } + + public String dashboardsOpenSearchRole() { + return dcm.getDashboardsOpenSearchRole(); + } + + public List getSignInOptions() { + return dcm.getSignInOptions(); + } + + public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege(PrivilegesEvaluationContext context, Set actions, String index) { + SecurityRoles securityRoles = getSecurityRoles(context.getMappedRoles()); + if (securityRoles.isPermittedOnSystemIndex(index)) { + return PrivilegesEvaluatorResponse.ok(); + } else { + return PrivilegesEvaluatorResponse.insufficient(ConfigConstants.SYSTEM_INDEX_PERMISSION); + } + } + + private Set evaluateAdditionalIndexPermissions(final ActionRequest request, final String originalAction) { + // --- check inner bulk requests + final Set additionalPermissionsRequired = new HashSet<>(); + + if (!isClusterPerm(originalAction)) { + additionalPermissionsRequired.add(originalAction); + } + + if (request instanceof ClusterSearchShardsRequest) { + additionalPermissionsRequired.add(SearchAction.NAME); + } + + if (request instanceof BulkShardRequest) { + BulkShardRequest bsr = (BulkShardRequest) request; + for (BulkItemRequest bir : bsr.items()) { + switch (bir.request().opType()) { + case CREATE: + additionalPermissionsRequired.add(IndexAction.NAME); + break; + case INDEX: + additionalPermissionsRequired.add(IndexAction.NAME); + break; + case DELETE: + additionalPermissionsRequired.add(DeleteAction.NAME); + break; + case UPDATE: + additionalPermissionsRequired.add(UpdateAction.NAME); + break; + } + } + } + + if (request instanceof IndicesAliasesRequest) { + IndicesAliasesRequest bsr = (IndicesAliasesRequest) request; + for (AliasActions bir : bsr.getAliasActions()) { + switch (bir.actionType()) { + case REMOVE_INDEX: + additionalPermissionsRequired.add(DeleteIndexAction.NAME); + break; + default: + break; + } + } + } + + if (request instanceof CreateIndexRequest) { + CreateIndexRequest cir = (CreateIndexRequest) request; + if (cir.aliases() != null && !cir.aliases().isEmpty()) { + additionalPermissionsRequired.add(IndicesAliasesAction.NAME); + } + } + + if (request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { + additionalPermissionsRequired.addAll(ConfigConstants.SECURITY_SNAPSHOT_RESTORE_NEEDED_WRITE_PRIVILEGES); + } + + if (additionalPermissionsRequired.size() > 1) { + traceAction("Additional permissions required: {}", additionalPermissionsRequired); + } + + if (log.isDebugEnabled() && additionalPermissionsRequired.size() > 1) { + log.debug("Additional permissions required: {}", additionalPermissionsRequired); + } + + return Collections.unmodifiableSet(additionalPermissionsRequired); + } + + public static boolean isClusterPerm(String action0) { + return (action0.startsWith("cluster:") + || action0.startsWith("indices:admin/template/") + || action0.startsWith("indices:admin/index_template/") + || action0.startsWith(SearchScrollAction.NAME) + || (action0.equals(BulkAction.NAME)) + || (action0.equals(MultiGetAction.NAME)) + || (action0.startsWith(MultiSearchAction.NAME)) + || (action0.equals(MultiTermVectorsAction.NAME)) + || (action0.equals(ReindexAction.NAME)) + || (action0.equals(RenderSearchTemplateAction.NAME))); + } + + @SuppressWarnings("unchecked") + private boolean checkFilteredAliases(Resolved requestedResolved, String action, boolean isDebugEnabled) { + final String faMode = dcm.getFilteredAliasMode();// getConfigSettings().dynamic.filtered_alias_mode; + + if (!"disallow".equals(faMode)) { + return false; + } + + if (!ACTION_MATCHER.test(action)) { + return false; + } + + Iterable indexMetaDataCollection; + + if (requestedResolved.isLocalAll()) { + indexMetaDataCollection = new Iterable() { + @Override + public Iterator iterator() { + return clusterService.state().getMetadata().getIndices().values().iterator(); + } + }; + } else { + Set indexMetaDataSet = new HashSet<>(requestedResolved.getAllIndices().size()); + + for (String requestAliasOrIndex : requestedResolved.getAllIndices()) { + IndexMetadata indexMetaData = clusterService.state().getMetadata().getIndices().get(requestAliasOrIndex); + if (indexMetaData == null) { + if (isDebugEnabled) { + log.debug("{} does not exist in cluster metadata", requestAliasOrIndex); + } + continue; + } + + indexMetaDataSet.add(indexMetaData); + } + + indexMetaDataCollection = indexMetaDataSet; + } + // check filtered aliases + for (IndexMetadata indexMetaData : indexMetaDataCollection) { + + final List filteredAliases = new ArrayList(); + + final Map aliases = indexMetaData.getAliases(); + + if (aliases != null && aliases.size() > 0) { + if (isDebugEnabled) { + log.debug("Aliases for {}: {}", indexMetaData.getIndex().getName(), aliases); + } + + final Iterator it = aliases.keySet().iterator(); + while (it.hasNext()) { + final String alias = it.next(); + final AliasMetadata aliasMetadata = aliases.get(alias); + + if (aliasMetadata != null && aliasMetadata.filteringRequired()) { + filteredAliases.add(aliasMetadata); + if (isDebugEnabled) { + log.debug("{} is a filtered alias {}", alias, aliasMetadata.getFilter()); + } + } else { + if (isDebugEnabled) { + log.debug("{} is not an alias or does not have a filter", alias); + } + } + } + } + + if (filteredAliases.size() > 1 && ACTION_MATCHER.test(action)) { + // TODO add queries as dls queries (works only if dls module is installed) + log.error( + "More than one ({}) filtered alias found for same index ({}). This is currently not supported. Aliases: {}", + filteredAliases.size(), + indexMetaData.getIndex().getName(), + toString(filteredAliases) + ); + return true; + } + } // end-for + + return false; + } + + private boolean checkDocAllowListHeader(User user, String action, ActionRequest request) { + String docAllowListHeader = threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DOC_ALLOWLIST_HEADER); + + if (docAllowListHeader == null) { + return false; + } + + if (!(request instanceof GetRequest)) { + return false; + } + + try { + DocumentAllowList documentAllowList = DocumentAllowList.parse(docAllowListHeader); + GetRequest getRequest = (GetRequest) request; + + if (documentAllowList.isAllowed(getRequest.index(), getRequest.id())) { + if (log.isDebugEnabled()) { + log.debug("Request " + request + " is allowed by " + documentAllowList); + } + + return true; + } else { + return false; + } + + } catch (Exception e) { + log.error("Error while handling document allow list: " + docAllowListHeader, e); + return false; + } + } + + private List toString(List aliases) { + if (aliases == null || aliases.size() == 0) { + return Collections.emptyList(); + } + + final List ret = new ArrayList<>(aliases.size()); + + for (final AliasMetadata amd : aliases) { + if (amd != null) { + ret.add(amd.alias()); + } + } + + return Collections.unmodifiableList(ret); + } +} diff --git a/src/main/java/org/opensearch/security/privileges/legacy/ProtectedIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/legacy/ProtectedIndexAccessEvaluator.java new file mode 100644 index 0000000000..b09e817ccc --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/legacy/ProtectedIndexAccessEvaluator.java @@ -0,0 +1,118 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.privileges.legacy; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.RealtimeRequest; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.tasks.Task; + +public class ProtectedIndexAccessEvaluator { + + protected final Logger log = LogManager.getLogger(this.getClass()); + + private final AuditLog auditLog; + private final WildcardMatcher indexMatcher; + private final WildcardMatcher allowedRolesMatcher; + private final Boolean protectedIndexEnabled; + private final WildcardMatcher deniedActionMatcher; + + public ProtectedIndexAccessEvaluator(final Settings settings, AuditLog auditLog) { + this.indexMatcher = WildcardMatcher.from( + settings.getAsList(ConfigConstants.SECURITY_PROTECTED_INDICES_KEY, ConfigConstants.SECURITY_PROTECTED_INDICES_DEFAULT) + ); + this.allowedRolesMatcher = WildcardMatcher.from( + settings.getAsList( + ConfigConstants.SECURITY_PROTECTED_INDICES_ROLES_KEY, + ConfigConstants.SECURITY_PROTECTED_INDICES_ROLES_DEFAULT + ) + ); + this.protectedIndexEnabled = settings.getAsBoolean( + ConfigConstants.SECURITY_PROTECTED_INDICES_ENABLED_KEY, + ConfigConstants.SECURITY_PROTECTED_INDICES_ENABLED_DEFAULT + ); + this.auditLog = auditLog; + + final List indexDeniedActionPatterns = new ArrayList(); + indexDeniedActionPatterns.add("indices:data/write*"); + indexDeniedActionPatterns.add("indices:admin/delete*"); + indexDeniedActionPatterns.add("indices:admin/mapping/delete*"); + indexDeniedActionPatterns.add("indices:admin/mapping/put*"); + indexDeniedActionPatterns.add("indices:admin/freeze*"); + indexDeniedActionPatterns.add("indices:admin/settings/update*"); + indexDeniedActionPatterns.add("indices:admin/aliases"); + indexDeniedActionPatterns.add("indices:admin/close*"); + indexDeniedActionPatterns.add("cluster:admin/snapshot/restore*"); + this.deniedActionMatcher = WildcardMatcher.from(indexDeniedActionPatterns); + } + + public PrivilegesEvaluatorResponse evaluate( + final ActionRequest request, + final Task task, + final String action, + final IndexResolverReplacer.Resolved requestedResolved, + final PrivilegesEvaluatorResponse presponse, + final Set mappedRoles + ) { + if (!protectedIndexEnabled) { + return presponse; + } + if (!requestedResolved.isLocalAll() + && indexMatcher.matchAny(requestedResolved.getAllIndices()) + && deniedActionMatcher.test(action) + && !allowedRolesMatcher.matchAny(mappedRoles)) { + auditLog.logMissingPrivileges(action, request, task); + log.warn("{} for '{}' index/indices is not allowed for a regular user", action, indexMatcher); + presponse.allowed = false; + return presponse.markComplete(); + } + + if (requestedResolved.isLocalAll() && deniedActionMatcher.test(action) && !allowedRolesMatcher.matchAny(mappedRoles)) { + auditLog.logMissingPrivileges(action, request, task); + log.warn("{} for '_all' indices is not allowed for a regular user", action); + presponse.allowed = false; + return presponse.markComplete(); + } + if ((requestedResolved.isLocalAll() || indexMatcher.matchAny(requestedResolved.getAllIndices())) + && !allowedRolesMatcher.matchAny(mappedRoles)) { + + final boolean isDebugEnabled = log.isDebugEnabled(); + if (request instanceof SearchRequest) { + ((SearchRequest) request).requestCache(Boolean.FALSE); + if (isDebugEnabled) { + log.debug("Disable search request cache for this request"); + } + } + + if (request instanceof RealtimeRequest) { + ((RealtimeRequest) request).realtime(Boolean.FALSE); + if (isDebugEnabled) { + log.debug("Disable realtime for this request"); + } + } + } + return presponse; + } +} diff --git a/src/main/java/org/opensearch/security/privileges/legacy/RestLayerPrivilegesEvaluatorImpl.java b/src/main/java/org/opensearch/security/privileges/legacy/RestLayerPrivilegesEvaluatorImpl.java new file mode 100644 index 0000000000..ec0b8f203c --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/legacy/RestLayerPrivilegesEvaluatorImpl.java @@ -0,0 +1,108 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.privileges.legacy; + +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; +import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.securityconf.SecurityRoles; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; +import org.opensearch.threadpool.ThreadPool; + +import org.greenrobot.eventbus.Subscribe; + +public class RestLayerPrivilegesEvaluatorImpl implements RestLayerPrivilegesEvaluator { + protected final Logger log = LogManager.getLogger(this.getClass()); + private final ClusterService clusterService; + private ThreadContext threadContext; + private ConfigModel configModel; + + public RestLayerPrivilegesEvaluatorImpl(final ClusterService clusterService, final ThreadPool threadPool) { + this.clusterService = clusterService; + this.threadContext = threadPool.getThreadContext(); + } + + @Subscribe + public void onConfigModelChanged(final ConfigModel configModel) { + this.configModel = configModel; + } + + SecurityRoles getSecurityRoles(final Set roles) { + return configModel.getSecurityRoles().filter(roles); + } + + boolean isInitialized() { + return configModel != null && configModel.getSecurityRoles() != null; + } + + public PrivilegesEvaluatorResponse evaluate(final User user, String routeName, final Set actions) { + if (!isInitialized()) { + throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); + } + + final PrivilegesEvaluatorResponse presponse = new PrivilegesEvaluatorResponse(); + + final TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + + final Set mappedRoles = mapRoles(user, caller); + + final SecurityRoles securityRoles = getSecurityRoles(mappedRoles); + + final boolean isDebugEnabled = log.isDebugEnabled(); + if (isDebugEnabled) { + log.debug("Evaluate permissions for {} on {}", user, clusterService.localNode().getName()); + log.debug("Action: {}", actions); + log.debug("Mapped roles: {}", mappedRoles.toString()); + } + + for (final String action : actions) { + if (!securityRoles.impliesClusterPermissionPermission(action)) { + presponse.allowed = false; + log.info( + "No permission match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}", + user, + action, + securityRoles.getRoleNames(), + action + ); + } else { + if (isDebugEnabled) { + log.debug("Allowed because we have permissions for {}", actions); + } + presponse.allowed = true; + + // break the loop as we found the matching permission + break; + } + } + + if (presponse.allowed) { + return PrivilegesEvaluatorResponse.ok(); + } else { + return PrivilegesEvaluatorResponse.insufficient(actions); + } + } + + Set mapRoles(final User user, final TransportAddress caller) { + return this.configModel.mapSecurityRoles(user, caller); + } +} diff --git a/src/main/java/org/opensearch/security/privileges/legacy/SnapshotRestoreEvaluator.java b/src/main/java/org/opensearch/security/privileges/legacy/SnapshotRestoreEvaluator.java new file mode 100644 index 0000000000..00485fc97a --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/legacy/SnapshotRestoreEvaluator.java @@ -0,0 +1,117 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.privileges.legacy; + +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.configuration.ClusterInfoHolder; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.SnapshotRestoreHelper; +import org.opensearch.tasks.Task; + +public class SnapshotRestoreEvaluator { + + protected final Logger log = LogManager.getLogger(this.getClass()); + private final boolean enableSnapshotRestorePrivilege; + private final String securityIndex; + private final AuditLog auditLog; + private final boolean restoreSecurityIndexEnabled; + + public SnapshotRestoreEvaluator(final Settings settings, AuditLog auditLog) { + this.enableSnapshotRestorePrivilege = settings.getAsBoolean( + ConfigConstants.SECURITY_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE, + ConfigConstants.SECURITY_DEFAULT_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE + ); + this.restoreSecurityIndexEnabled = settings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_RESTORE_SECURITYINDEX_ENABLED, false); + + this.securityIndex = settings.get( + ConfigConstants.SECURITY_CONFIG_INDEX_NAME, + ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX + ); + this.auditLog = auditLog; + } + + public PrivilegesEvaluatorResponse evaluate( + final ActionRequest request, + final Task task, + final String action, + final ClusterInfoHolder clusterInfoHolder, + final PrivilegesEvaluatorResponse presponse + ) { + + if (!(request instanceof RestoreSnapshotRequest)) { + return presponse; + } + + // snapshot restore for regular users not enabled + if (!enableSnapshotRestorePrivilege) { + log.warn("{} is not allowed for a regular user", action); + presponse.allowed = false; + return presponse.markComplete(); + } + + // if this feature is enabled, users can also snapshot and restore + // the Security index and the global state + if (restoreSecurityIndexEnabled) { + presponse.allowed = true; + return presponse; + } + + if (clusterInfoHolder.isLocalNodeElectedClusterManager() == Boolean.FALSE) { + presponse.allowed = true; + return presponse.markComplete(); + } + + final RestoreSnapshotRequest restoreRequest = (RestoreSnapshotRequest) request; + + // Do not allow restore of global state + if (restoreRequest.includeGlobalState()) { + auditLog.logSecurityIndexAttempt(request, action, task); + log.warn("{} with 'include_global_state' enabled is not allowed", action); + presponse.allowed = false; + return presponse.markComplete(); + } + + final List rs = SnapshotRestoreHelper.resolveOriginalIndices(restoreRequest); + + if (rs != null && (rs.contains(securityIndex) || rs.contains("_all") || rs.contains("*"))) { + auditLog.logSecurityIndexAttempt(request, action, task); + log.warn("{} for '{}' as source index is not allowed", action, securityIndex); + presponse.allowed = false; + return presponse.markComplete(); + } + return presponse; + } +} diff --git a/src/main/java/org/opensearch/security/privileges/legacy/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/legacy/SystemIndexAccessEvaluator.java new file mode 100644 index 0000000000..2502d239ce --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/legacy/SystemIndexAccessEvaluator.java @@ -0,0 +1,364 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.privileges.legacy; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.RealtimeRequest; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.indices.SystemIndexRegistry; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; +import org.opensearch.security.securityconf.SecurityRoles; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.security.user.User; +import org.opensearch.tasks.Task; + +/** + * This class performs authorization on requests targeting system indices + * NOTE: + * - The term `protected system indices` used here translates to system indices + * which have an added layer of security and cannot be accessed by anyone except Super Admin + */ +public class SystemIndexAccessEvaluator { + + Logger log = LogManager.getLogger(this.getClass()); + + private final String securityIndex; + private final AuditLog auditLog; + private final IndexResolverReplacer irr; + private final boolean filterSecurityIndex; + // for system-indices configuration + private final WildcardMatcher systemIndexMatcher; + private final WildcardMatcher superAdminAccessOnlyIndexMatcher; + private final WildcardMatcher deniedActionsMatcher; + + private final boolean isSystemIndexEnabled; + private final boolean isSystemIndexPermissionEnabled; + + public SystemIndexAccessEvaluator(final Settings settings, AuditLog auditLog, IndexResolverReplacer irr) { + this.securityIndex = settings.get( + ConfigConstants.SECURITY_CONFIG_INDEX_NAME, + ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX + ); + this.auditLog = auditLog; + this.irr = irr; + this.filterSecurityIndex = settings.getAsBoolean(ConfigConstants.SECURITY_FILTER_SECURITYINDEX_FROM_ALL_REQUESTS, false); + this.systemIndexMatcher = WildcardMatcher.from( + settings.getAsList(ConfigConstants.SECURITY_SYSTEM_INDICES_KEY, ConfigConstants.SECURITY_SYSTEM_INDICES_DEFAULT) + ); + + this.superAdminAccessOnlyIndexMatcher = WildcardMatcher.from(this.securityIndex); + this.isSystemIndexEnabled = settings.getAsBoolean( + ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY, + ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_DEFAULT + ); + final boolean restoreSecurityIndexEnabled = settings.getAsBoolean( + ConfigConstants.SECURITY_UNSUPPORTED_RESTORE_SECURITYINDEX_ENABLED, + false + ); + + final List deniedActionPatternsList = deniedActionPatterns(); + + final List deniedActionPatternsListNoSnapshot = new ArrayList<>(deniedActionPatternsList); + deniedActionPatternsListNoSnapshot.add("indices:admin/close*"); + deniedActionPatternsListNoSnapshot.add("cluster:admin/snapshot/restore*"); + + deniedActionsMatcher = WildcardMatcher.from( + restoreSecurityIndexEnabled ? deniedActionPatternsList : deniedActionPatternsListNoSnapshot + ); + isSystemIndexPermissionEnabled = settings.getAsBoolean( + ConfigConstants.SECURITY_SYSTEM_INDICES_PERMISSIONS_ENABLED_KEY, + ConfigConstants.SECURITY_SYSTEM_INDICES_PERMISSIONS_DEFAULT + ); + } + + private static List deniedActionPatterns() { + final List securityIndexDeniedActionPatternsList = new ArrayList<>(); + securityIndexDeniedActionPatternsList.add("indices:data/write*"); + securityIndexDeniedActionPatternsList.add("indices:admin/delete*"); + securityIndexDeniedActionPatternsList.add("indices:admin/mapping/delete*"); + securityIndexDeniedActionPatternsList.add("indices:admin/mapping/put*"); + securityIndexDeniedActionPatternsList.add("indices:admin/freeze*"); + securityIndexDeniedActionPatternsList.add("indices:admin/settings/update*"); + securityIndexDeniedActionPatternsList.add("indices:admin/aliases"); + return securityIndexDeniedActionPatternsList; + } + + public PrivilegesEvaluatorResponse evaluate( + final ActionRequest request, + final Task task, + final String action, + final Resolved requestedResolved, + final PrivilegesEvaluatorResponse presponse, + final SecurityRoles securityRoles, + final User user, + final IndexNameExpressionResolver resolver, + final ClusterService clusterService + ) { + evaluateSystemIndicesAccess(action, requestedResolved, request, task, presponse, securityRoles, user, resolver, clusterService); + + if (requestedResolved.isLocalAll() + || requestedResolved.getAllIndices().contains(securityIndex) + || requestContainsAnySystemIndices(requestedResolved)) { + + if (request instanceof SearchRequest) { + ((SearchRequest) request).requestCache(Boolean.FALSE); + if (log.isDebugEnabled()) { + log.debug("Disable search request cache for this request"); + } + } + + if (request instanceof RealtimeRequest) { + ((RealtimeRequest) request).realtime(Boolean.FALSE); + if (log.isDebugEnabled()) { + log.debug("Disable realtime for this request"); + } + } + } + return presponse; + } + + /** + * Checks if request is for any system index + * @param requestedResolved request which contains indices to be matched against system indices + * @return true if a match is found, false otherwise + */ + private boolean requestContainsAnySystemIndices(final Resolved requestedResolved) { + return !getAllSystemIndices(requestedResolved).isEmpty(); + } + + /** + * Gets all indices requested in the original request. + * It will always return security index if it is present in the request, as security index is protected regardless + * of feature being enabled or disabled + * @param requestedResolved request which contains indices to be matched against system indices + * @return the set of protected system indices present in the request + */ + private Set getAllSystemIndices(final Resolved requestedResolved) { + final Set systemIndices = requestedResolved.getAllIndices() + .stream() + .filter(securityIndex::equals) + .collect(Collectors.toSet()); + if (isSystemIndexEnabled) { + systemIndices.addAll(systemIndexMatcher.getMatchAny(requestedResolved.getAllIndices(), Collectors.toList())); + systemIndices.addAll(SystemIndexRegistry.matchesSystemIndexPattern(requestedResolved.getAllIndices())); + } + return systemIndices; + } + + /** + * Checks if request contains any system index that is non-permission-able + * NOTE: Security index is currently non-permission-able + * @param requestedResolved request which contains indices to be matched against non-permission-able system indices + * @return true if the request contains any non-permission-able index,false otherwise + */ + private boolean requestContainsAnyProtectedSystemIndices(final Resolved requestedResolved) { + return !getAllProtectedSystemIndices(requestedResolved).isEmpty(); + } + + /** + * Filters the request to get all system indices that are protected and are non-permission-able + * @param requestedResolved request which contains indices to be matched against non-permission-able system indices + * @return the list of protected system indices present in the request + */ + private List getAllProtectedSystemIndices(final Resolved requestedResolved) { + return new ArrayList<>(superAdminAccessOnlyIndexMatcher.getMatchAny(requestedResolved.getAllIndices(), Collectors.toList())); + } + + /** + * Checks if the request contains any regular (non-system and non-protected) indices. + * Regular indices are those that are not categorized as system indices or protected system indices. + * This method helps in identifying requests that might be accessing regular indices alongside system indices. + * @param requestedResolved The resolved object of the request, which contains the list of indices from the original request. + * @return true if the request contains any regular indices, false otherwise. + */ + private boolean requestContainsAnyRegularIndices(final Resolved requestedResolved) { + Set allIndices = requestedResolved.getAllIndices(); + + Set allSystemIndices = getAllSystemIndices(requestedResolved); + List allProtectedSystemIndices = getAllProtectedSystemIndices(requestedResolved); + + return allIndices.stream().anyMatch(index -> !allSystemIndices.contains(index) && !allProtectedSystemIndices.contains(index)); + } + + /** + * Is the current action allowed to be performed on security index + * @param action request action on security index + * @return true if action is allowed, false otherwise + */ + private boolean isActionAllowed(String action) { + return deniedActionsMatcher.test(action); + } + + /** + * Perform access check on requested indices and actions for those indices + * @param action action to be performed on request indices + * @param requestedResolved this object contains all indices this request is resolved to + * @param request the action request to be used for audit logging + * @param task task in which this access check will be performed + * @param presponse the pre-response object that will eventually become a response and returned to the requester + * @param securityRoles user's roles which will be used for access evaluation + * @param user this user's permissions will be looked up + * @param resolver the index expression resolver + * @param clusterService required to fetch cluster state metadata + */ + private void evaluateSystemIndicesAccess( + final String action, + final Resolved requestedResolved, + final ActionRequest request, + final Task task, + final PrivilegesEvaluatorResponse presponse, + SecurityRoles securityRoles, + final User user, + final IndexNameExpressionResolver resolver, + final ClusterService clusterService + ) { + // Perform access check is system index permissions are enabled + boolean containsSystemIndex = requestContainsAnySystemIndices(requestedResolved); + boolean containsRegularIndex = requestContainsAnyRegularIndices(requestedResolved); + boolean serviceAccountUser = user.isServiceAccount(); + + if (isSystemIndexPermissionEnabled) { + if (serviceAccountUser && containsRegularIndex) { + auditLog.logSecurityIndexAttempt(request, action, task); + if (!containsSystemIndex && log.isInfoEnabled()) { + log.info("{} not permitted for a service account {} on non-system indices.", action, securityRoles); + } else if (containsSystemIndex && log.isDebugEnabled()) { + List regularIndices = requestedResolved.getAllIndices() + .stream() + .filter( + index -> !getAllSystemIndices(requestedResolved).contains(index) + && !getAllProtectedSystemIndices(requestedResolved).contains(index) + ) + .collect(Collectors.toList()); + log.debug("Service account cannot access regular indices: {}", regularIndices); + } + presponse.allowed = false; + presponse.markComplete(); + return; + } + boolean containsProtectedIndex = requestContainsAnyProtectedSystemIndices(requestedResolved); + if (containsProtectedIndex) { + auditLog.logSecurityIndexAttempt(request, action, task); + if (log.isInfoEnabled()) { + log.info( + "{} not permitted for a regular user {} on protected system indices {}", + action, + securityRoles, + String.join(", ", getAllProtectedSystemIndices(requestedResolved)) + ); + } + presponse.allowed = false; + presponse.markComplete(); + return; + } else if (containsSystemIndex + && !securityRoles.hasExplicitIndexPermission( + requestedResolved, + user, + new String[] { ConfigConstants.SYSTEM_INDEX_PERMISSION }, + resolver, + clusterService + )) { + auditLog.logSecurityIndexAttempt(request, action, task); + if (log.isInfoEnabled()) { + log.info( + "No {} permission for user roles {} to System Indices {}", + action, + securityRoles, + String.join(", ", getAllSystemIndices(requestedResolved)) + ); + } + presponse.allowed = false; + presponse.markComplete(); + return; + } + } + + if (isActionAllowed(action)) { + if (requestedResolved.isLocalAll()) { + if (filterSecurityIndex) { + irr.replace(request, false, "*", "-" + securityIndex); + if (log.isDebugEnabled()) { + log.debug( + "Filtered '{}' from {}, resulting list with *,-{} is {}", + securityIndex, + requestedResolved, + securityIndex, + irr.resolveRequest(request) + ); + } + } else { + auditLog.logSecurityIndexAttempt(request, action, task); + log.warn("{} for '_all' indices is not allowed for a regular user", action); + presponse.allowed = false; + presponse.markComplete(); + } + } + // if system index is enabled and system index permissions are enabled we don't need to perform any further + // checks as it has already been performed via hasExplicitIndexPermission + else if (containsSystemIndex && !isSystemIndexPermissionEnabled) { + if (filterSecurityIndex) { + Set allWithoutSecurity = new HashSet<>(requestedResolved.getAllIndices()); + allWithoutSecurity.remove(securityIndex); + if (allWithoutSecurity.isEmpty()) { + if (log.isDebugEnabled()) { + log.debug("Filtered '{}' but resulting list is empty", securityIndex); + } + presponse.allowed = false; + presponse.markComplete(); + return; + } + irr.replace(request, false, allWithoutSecurity.toArray(new String[0])); + if (log.isDebugEnabled()) { + log.debug("Filtered '{}', resulting list is {}", securityIndex, allWithoutSecurity); + } + } else { + auditLog.logSecurityIndexAttempt(request, action, task); + final String foundSystemIndexes = String.join(", ", getAllSystemIndices(requestedResolved)); + log.warn("{} for '{}' index is not allowed for a regular user", action, foundSystemIndexes); + presponse.allowed = false; + presponse.markComplete(); + } + } + } + } +} diff --git a/src/main/java/org/opensearch/security/privileges/legacy/TermsAggregationEvaluator.java b/src/main/java/org/opensearch/security/privileges/legacy/TermsAggregationEvaluator.java new file mode 100644 index 0000000000..2a750ef69b --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/legacy/TermsAggregationEvaluator.java @@ -0,0 +1,119 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed 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. + */ + +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.privileges.legacy; + +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.fieldcaps.FieldCapabilitiesAction; +import org.opensearch.action.get.GetAction; +import org.opensearch.action.get.MultiGetAction; +import org.opensearch.action.search.MultiSearchAction; +import org.opensearch.action.search.SearchAction; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.index.query.MatchNoneQueryBuilder; +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.index.query.TermsQueryBuilder; +import org.opensearch.search.aggregations.AggregationBuilder; +import org.opensearch.search.aggregations.bucket.terms.TermsAggregationBuilder; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; +import org.opensearch.security.securityconf.SecurityRoles; +import org.opensearch.security.user.User; + +public class TermsAggregationEvaluator { + + protected final Logger log = LogManager.getLogger(this.getClass()); + + private static final String[] READ_ACTIONS = new String[] { + MultiSearchAction.NAME, + MultiGetAction.NAME, + GetAction.NAME, + SearchAction.NAME, + FieldCapabilitiesAction.NAME }; + + private static final QueryBuilder NONE_QUERY = new MatchNoneQueryBuilder(); + + public TermsAggregationEvaluator() {} + + public PrivilegesEvaluatorResponse evaluate( + final Resolved resolved, + final ActionRequest request, + ClusterService clusterService, + User user, + SecurityRoles securityRoles, + IndexNameExpressionResolver resolver, + PrivilegesEvaluatorResponse presponse + ) { + try { + if (request instanceof SearchRequest) { + SearchRequest sr = (SearchRequest) request; + + if (sr.source() != null + && sr.source().query() == null + && sr.source().aggregations() != null + && sr.source().aggregations().getAggregatorFactories() != null + && sr.source().aggregations().getAggregatorFactories().size() == 1 + && sr.source().size() == 0) { + AggregationBuilder ab = sr.source().aggregations().getAggregatorFactories().iterator().next(); + if (ab instanceof TermsAggregationBuilder && "terms".equals(ab.getType()) && "indices".equals(ab.getName())) { + if ("_index".equals(((TermsAggregationBuilder) ab).field()) + && ab.getPipelineAggregations().isEmpty() + && ab.getSubAggregations().isEmpty()) { + + final Set allPermittedIndices = securityRoles.getAllPermittedIndicesForDashboards( + resolved, + user, + READ_ACTIONS, + resolver, + clusterService + ); + if (allPermittedIndices == null || allPermittedIndices.isEmpty()) { + sr.source().query(NONE_QUERY); + } else { + sr.source().query(new TermsQueryBuilder("_index", allPermittedIndices)); + } + + presponse.allowed = true; + return presponse.markComplete(); + } + } + } + } + } catch (Exception e) { + log.warn("Unable to evaluate terms aggregation", e); + return presponse; + } + + return presponse; + } +} diff --git a/src/main/java/org/opensearch/security/resolver/IndexResolverReplacer.java b/src/main/java/org/opensearch/security/resolver/IndexResolverReplacer.java index 4a4e714348..ebca3e652e 100644 --- a/src/main/java/org/opensearch/security/resolver/IndexResolverReplacer.java +++ b/src/main/java/org/opensearch/security/resolver/IndexResolverReplacer.java @@ -30,6 +30,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.EnumSet; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -37,6 +38,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Supplier; import java.util.regex.PatternSyntaxException; import java.util.stream.Collectors; @@ -104,13 +106,17 @@ public class IndexResolverReplacer { private static final Set NULL_SET = new HashSet<>(Collections.singleton(null)); private final Logger log = LogManager.getLogger(this.getClass()); private final IndexNameExpressionResolver resolver; - private final ClusterService clusterService; + private final Supplier clusterStateSupplier; private final ClusterInfoHolder clusterInfoHolder; private volatile boolean respectRequestIndicesOptions = false; - public IndexResolverReplacer(IndexNameExpressionResolver resolver, ClusterService clusterService, ClusterInfoHolder clusterInfoHolder) { + public IndexResolverReplacer( + IndexNameExpressionResolver resolver, + Supplier clusterStateSupplier, + ClusterInfoHolder clusterInfoHolder + ) { this.resolver = resolver; - this.clusterService = clusterService; + this.clusterStateSupplier = clusterStateSupplier; this.clusterInfoHolder = clusterInfoHolder; } @@ -236,10 +242,10 @@ private void resolveIndexPatterns( final RemoteClusterService remoteClusterService = OpenSearchSecurityPlugin.GuiceHolder.getRemoteClusterService(); - if (remoteClusterService.isCrossClusterSearchEnabled() && enableCrossClusterResolution) { + if (remoteClusterService != null && remoteClusterService.isCrossClusterSearchEnabled() && enableCrossClusterResolution) { remoteIndices = new HashSet<>(); final Map remoteClusterIndices = OpenSearchSecurityPlugin.GuiceHolder.getRemoteClusterService() - .groupIndices(indicesOptions, original, idx -> resolver.hasIndexAbstraction(idx, clusterService.state())); + .groupIndices(indicesOptions, original, idx -> resolver.hasIndexAbstraction(idx, clusterStateSupplier.get())); final Set remoteClusters = remoteClusterIndices.keySet() .stream() .filter(k -> !RemoteClusterService.LOCAL_CLUSTER_GROUP_KEY.equals(k)) @@ -292,7 +298,7 @@ private void resolveIndexPatterns( } else { - final ClusterState state = clusterService.state(); + final ClusterState state = clusterStateSupplier.get(); final Set dateResolvedLocalRequestedPatterns = localRequestedPatterns.stream() .map(resolver::resolveDateMathExpression) .collect(Collectors.toSet()); @@ -425,6 +431,10 @@ public String[] provide(String[] original, Object request, boolean supportsRepla }, false); } + public boolean replace(final TransportRequest request, boolean retainMode, Collection replacements) { + return replace(request, retainMode, replacements.toArray(new String[replacements.size()])); + } + public Resolved resolveRequest(final Object request) { if (log.isDebugEnabled()) { log.debug("Resolve aliases, indices and types from {}", request.getClass().getSimpleName()); @@ -449,6 +459,11 @@ public final static class Resolved { SearchRequest.DEFAULT_INDICES_OPTIONS ); + private static final IndicesOptions EXACT_INDEX_OPTIONS = new IndicesOptions( + EnumSet.of(IndicesOptions.Option.FORBID_ALIASES_TO_MULTIPLE_INDICES), + EnumSet.noneOf(IndicesOptions.WildcardStates.class) + ); + private final Set aliases; private final Set allIndices; private final Set originalRequested; @@ -485,8 +500,12 @@ public Set getAllIndices() { } public Set getAllIndicesResolved(ClusterService clusterService, IndexNameExpressionResolver resolver) { + return getAllIndicesResolved(clusterService::state, resolver); + } + + public Set getAllIndicesResolved(Supplier clusterStateSupplier, IndexNameExpressionResolver resolver) { if (isLocalAll) { - return new HashSet<>(Arrays.asList(resolver.concreteIndexNames(clusterService.state(), indicesOptions, "*"))); + return new HashSet<>(Arrays.asList(resolver.concreteIndexNames(clusterStateSupplier.get(), indicesOptions, "*"))); } else { return allIndices; } @@ -550,6 +569,11 @@ public boolean equals(Object obj) { } else if (!remoteIndices.equals(other.remoteIndices)) return false; return true; } + + public static Resolved ofIndex(String index) { + ImmutableSet indexSet = ImmutableSet.of(index); + return new Resolved(ImmutableSet.of(), indexSet, indexSet, ImmutableSet.of(), EXACT_INDEX_OPTIONS); + } } private List renamedIndices(final RestoreSnapshotRequest request, final List filteredIndices) { diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java index f7105e2386..fdc66fdb31 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java @@ -105,7 +105,7 @@ private void loadStaticConfig() throws IOException { staticTenants = SecurityDynamicConfiguration.fromNode(staticTenantsJsonNode, CType.TENANTS, 2, 0, 0); } - public final static SecurityDynamicConfiguration addStatics(SecurityDynamicConfiguration original) { + public final static SecurityDynamicConfiguration addStatics(SecurityDynamicConfiguration original) { if (original.getCType() == CType.ACTIONGROUPS && !staticActionGroups.getCEntries().isEmpty()) { original.add(staticActionGroups.deepClone()); } diff --git a/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java b/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java index d99c9ad2d0..1237c917bf 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java @@ -243,6 +243,20 @@ public static SecurityDynamicConfiguration fromNode(JsonNode json, CType< ); } + /** + * For testing only + */ + public static SecurityDynamicConfiguration fromYaml(String yaml, CType ctype) throws JsonProcessingException { + Class implementationClass = ctype.getConfigClass(); + SecurityDynamicConfiguration result = DefaultObjectMapper.YAML_MAPPER.readValue( + yaml, + DefaultObjectMapper.getTypeFactory().constructParametricType(SecurityDynamicConfiguration.class, implementationClass) + ); + result.ctype = ctype; + result.version = 2; + return result; + } + // for Jackson private SecurityDynamicConfiguration() { super(); @@ -416,6 +430,18 @@ public Class getImplementingClass() { } @SuppressWarnings("unchecked") + @JsonIgnore + public SecurityDynamicConfiguration clone() { + SecurityDynamicConfiguration result = new SecurityDynamicConfiguration(); + result.version = this.version; + result.ctype = this.ctype; + result.primaryTerm = this.primaryTerm; + result.seqNo = this.seqNo; + result._meta = this._meta; + result.centries.putAll(this.centries); + return result; + } + @JsonIgnore public SecurityDynamicConfiguration deepClone() { try { @@ -431,6 +457,7 @@ public SecurityDynamicConfiguration deepClone() { return result; } else { // We are on a pre-v7 config version. This can be only if we skipped auto conversion. So, we do here the same. + @SuppressWarnings("unchecked") SecurityDynamicConfiguration result = (SecurityDynamicConfiguration) fromJsonWithoutAutoConversion( DefaultObjectMapper.writeValueAsString(this, false), ctypeUnsafe, diff --git a/src/main/java/org/opensearch/security/support/WildcardMatcher.java b/src/main/java/org/opensearch/security/support/WildcardMatcher.java index d811a73730..537e2d473c 100644 --- a/src/main/java/org/opensearch/security/support/WildcardMatcher.java +++ b/src/main/java/org/opensearch/security/support/WildcardMatcher.java @@ -28,6 +28,7 @@ import java.util.Arrays; import java.util.Collection; +import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -282,6 +283,55 @@ public Optional findFirst(final String candidate) { return Optional.ofNullable(test(candidate) ? this : null); } + public Iterable iterateMatching(Iterable candidates) { + return iterateMatching(candidates, Function.identity()); + } + + public Iterable iterateMatching(Iterable candidates, Function toStringFunction) { + return new Iterable() { + + @Override + public Iterator iterator() { + Iterator delegate = candidates.iterator(); + + return new Iterator() { + private E next; + + @Override + public boolean hasNext() { + if (next == null) { + init(); + } + + return next != null; + } + + @Override + public E next() { + if (next == null) { + init(); + } + + E result = next; + next = null; + return result; + } + + private void init() { + while (delegate.hasNext()) { + E candidate = delegate.next(); + + if (test(toStringFunction.apply(candidate))) { + next = candidate; + break; + } + } + } + }; + } + }; + } + public static List matchers(Collection patterns) { return patterns.stream().map(p -> WildcardMatcher.from(p, true)).collect(Collectors.toList()); } @@ -294,6 +344,10 @@ public static List getAllMatchingPatterns(final Collection p.matchAny(candidates)).map(Objects::toString).collect(Collectors.toList()); } + public static boolean isExact(String pattern) { + return pattern == null || !(pattern.contains("*") || pattern.contains("?") || (pattern.startsWith("/") && pattern.endsWith("/"))); + } + // // --- Implementation specializations --- // diff --git a/src/test/java/org/opensearch/security/AggregationTests.java b/src/test/java/org/opensearch/security/AggregationTests.java index bdd51960d0..a858d37ea9 100644 --- a/src/test/java/org/opensearch/security/AggregationTests.java +++ b/src/test/java/org/opensearch/security/AggregationTests.java @@ -26,6 +26,11 @@ package org.opensearch.security; +import java.util.Arrays; +import java.util.Collection; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import org.apache.http.HttpStatus; import org.junit.Test; @@ -37,6 +42,7 @@ import org.opensearch.client.Client; import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentType; +import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.test.SingleClusterTest; import org.opensearch.security.test.helper.rest.RestHelper; import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; @@ -45,10 +51,22 @@ import static org.hamcrest.Matchers.is; public class AggregationTests extends SingleClusterTest { + private boolean useOldPrivilegeEvaluationImplementation; + + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + + public AggregationTests(@Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation) { + this.useOldPrivilegeEvaluationImplementation = useOldPrivilegeEvaluationImplementation; + } @Test public void testBasicAggregations() throws Exception { - final Settings settings = Settings.builder().build(); + final Settings settings = Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(); setup(settings); final RestHelper rh = nonSslRestHelper(); diff --git a/src/test/java/org/opensearch/security/DataStreamIntegrationTests.java b/src/test/java/org/opensearch/security/DataStreamIntegrationTests.java index 6ca83c83dc..842406d149 100644 --- a/src/test/java/org/opensearch/security/DataStreamIntegrationTests.java +++ b/src/test/java/org/opensearch/security/DataStreamIntegrationTests.java @@ -11,10 +11,17 @@ package org.opensearch.security; +import java.util.Arrays; +import java.util.Collection; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.test.SingleClusterTest; import org.opensearch.security.test.helper.rest.RestHelper; import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; @@ -23,6 +30,16 @@ import static org.hamcrest.Matchers.is; public class DataStreamIntegrationTests extends SingleClusterTest { + private boolean useOldPrivilegeEvaluationImplementation; + + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + + public DataStreamIntegrationTests(@Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation) { + this.useOldPrivilegeEvaluationImplementation = useOldPrivilegeEvaluationImplementation; + } final String bulkDocsBody = "{ \"create\" : {} }" + System.lineSeparator() @@ -58,7 +75,12 @@ public void createSampleDataStreams(RestHelper rh) throws Exception { @Test public void testCreateDataStream() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); + RestHelper rh = nonSslRestHelper(); HttpResponse response; @@ -95,7 +117,11 @@ public void testCreateDataStream() throws Exception { @Test public void testGetDataStream() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); RestHelper rh = nonSslRestHelper(); createSampleDataStreams(rh); HttpResponse response; @@ -140,7 +166,11 @@ public void testGetDataStream() throws Exception { @Test public void testDeleteDataStream() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); RestHelper rh = nonSslRestHelper(); createSampleDataStreams(rh); HttpResponse response; @@ -188,7 +218,11 @@ public void testDeleteDataStream() throws Exception { @Test public void testDataStreamStats() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); RestHelper rh = nonSslRestHelper(); createSampleDataStreams(rh); HttpResponse response; @@ -236,7 +270,11 @@ public void testDataStreamStats() throws Exception { @Test public void testGetIndexOnBackingIndicesOfDataStream() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); RestHelper rh = nonSslRestHelper(); createSampleDataStreams(rh); HttpResponse response; @@ -281,7 +319,11 @@ public void testGetIndexOnBackingIndicesOfDataStream() throws Exception { @Test public void testDocumentLevelSecurityOnDataStream() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); RestHelper rh = nonSslRestHelper(); createSampleDataStreams(rh); HttpResponse response; @@ -350,7 +392,11 @@ public void testDocumentLevelSecurityOnDataStream() throws Exception { @Test public void testFLSOnBackingIndicesOfDataStream() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); RestHelper rh = nonSslRestHelper(); createSampleDataStreams(rh); HttpResponse response; @@ -428,7 +474,11 @@ public void testFLSOnBackingIndicesOfDataStream() throws Exception { @Test public void testFieldMaskingOnDataStream() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); RestHelper rh = nonSslRestHelper(); createSampleDataStreams(rh); HttpResponse response; diff --git a/src/test/java/org/opensearch/security/HttpIntegrationTests.java b/src/test/java/org/opensearch/security/HttpIntegrationTests.java index a932ba98e1..e4a533a36c 100644 --- a/src/test/java/org/opensearch/security/HttpIntegrationTests.java +++ b/src/test/java/org/opensearch/security/HttpIntegrationTests.java @@ -29,7 +29,11 @@ import java.io.File; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.util.Arrays; +import java.util.Collection; +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import com.fasterxml.jackson.databind.JsonNode; import org.apache.http.HttpStatus; import org.apache.http.NoHttpResponseException; @@ -49,6 +53,7 @@ import org.opensearch.security.action.configupdate.ConfigUpdateAction; import org.opensearch.security.action.configupdate.ConfigUpdateRequest; import org.opensearch.security.action.configupdate.ConfigUpdateResponse; +import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.ssl.util.SSLConfigConstants; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.test.DynamicSecurityConfig; @@ -62,11 +67,22 @@ import static org.opensearch.security.DefaultObjectMapper.readTree; public class HttpIntegrationTests extends SingleClusterTest { + private boolean useOldPrivilegeEvaluationImplementation; + + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + + public HttpIntegrationTests(@Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation) { + this.useOldPrivilegeEvaluationImplementation = useOldPrivilegeEvaluationImplementation; + } @Test public void testHTTPBasic() throws Exception { final Settings settings = Settings.builder() .putList(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + ".worf", "knuddel", "nonexists") + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) .build(); setup(settings); final RestHelper rh = nonSslRestHelper(); @@ -876,7 +892,10 @@ public void testHTTPBasic2() throws Exception { @Test public void testBulk() throws Exception { - final Settings settings = Settings.builder().put(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH").build(); + final Settings settings = Settings.builder() + .put(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH") + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(); setup(Settings.EMPTY, new DynamicSecurityConfig().setSecurityRoles("roles_bulk.yml"), settings); final RestHelper rh = nonSslRestHelper(); @@ -897,7 +916,10 @@ public void testBulk() throws Exception { @Test public void testBulkWithOneIndexFailure() throws Exception { - final Settings settings = Settings.builder().put(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH").build(); + final Settings settings = Settings.builder() + .put(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH") + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(); setup(Settings.EMPTY, new DynamicSecurityConfig().setSecurityRoles("roles_bulk.yml"), settings); final RestHelper rh = nonSslRestHelper(); @@ -920,7 +942,10 @@ public void testBulkWithOneIndexFailure() throws Exception { @Test public void test557() throws Exception { - final Settings settings = Settings.builder().put(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH").build(); + final Settings settings = Settings.builder() + .put(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH") + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(); setup(Settings.EMPTY, new DynamicSecurityConfig(), settings); try (Client tc = getClient()) { @@ -959,7 +984,10 @@ public void test557() throws Exception { @Test public void testITT1635() throws Exception { - final Settings settings = Settings.builder().put(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH").build(); + final Settings settings = Settings.builder() + .put(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH") + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(); setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_dnfof.yml").setSecurityRoles("roles_itt1635.yml"), settings); try (Client tc = getClient()) { @@ -1020,7 +1048,9 @@ public void testITT1635() throws Exception { @Test public void testTenantInfo() throws Exception { - final Settings settings = Settings.builder().build(); + final Settings settings = Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(); setup(Settings.EMPTY, new DynamicSecurityConfig(), settings); /* @@ -1111,6 +1141,7 @@ public void testTenantInfo() throws Exception { public void testRestImpersonation() throws Exception { final Settings settings = Settings.builder() .putList(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + ".worf", "someotherusernotininternalusersfile") + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) .build(); setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_rest_impersonation.yml"), settings); final RestHelper rh = nonSslRestHelper(); @@ -1147,7 +1178,9 @@ public void testSslOnlyMode() throws Exception { @Test public void testAll() throws Exception { - final Settings settings = Settings.builder().build(); + final Settings settings = Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(); setup(settings); final RestHelper rh = nonSslRestHelper(); diff --git a/src/test/java/org/opensearch/security/IndexIntegrationTests.java b/src/test/java/org/opensearch/security/IndexIntegrationTests.java index 648a9b1ade..7554137a54 100644 --- a/src/test/java/org/opensearch/security/IndexIntegrationTests.java +++ b/src/test/java/org/opensearch/security/IndexIntegrationTests.java @@ -28,9 +28,13 @@ import java.net.URLEncoder; import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collection; import java.util.Date; import java.util.TimeZone; +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Ignore; @@ -48,6 +52,7 @@ import org.opensearch.common.xcontent.XContentType; import org.opensearch.index.query.QueryBuilders; import org.opensearch.indices.InvalidIndexNameException; +import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.SecurityUtils; import org.opensearch.security.test.DynamicSecurityConfig; @@ -59,6 +64,16 @@ import static org.hamcrest.Matchers.is; public class IndexIntegrationTests extends SingleClusterTest { + private boolean useOldPrivilegeEvaluationImplementation; + + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + + public IndexIntegrationTests(@Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation) { + this.useOldPrivilegeEvaluationImplementation = useOldPrivilegeEvaluationImplementation; + } @Test public void testComposite() throws Exception { @@ -66,7 +81,9 @@ public void testComposite() throws Exception { setup( Settings.EMPTY, new DynamicSecurityConfig().setConfig("composite_config.yml").setSecurityRoles("roles_composite.yml"), - Settings.EMPTY, + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(), true ); final RestHelper rh = nonSslRestHelper(); @@ -105,7 +122,14 @@ public void testComposite() throws Exception { @Test public void testBulkShards() throws Exception { - setup(Settings.EMPTY, new DynamicSecurityConfig().setSecurityRoles("roles_bs.yml"), Settings.EMPTY, true); + setup( + Settings.EMPTY, + new DynamicSecurityConfig().setSecurityRoles("roles_bs.yml"), + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(), + true + ); final RestHelper rh = nonSslRestHelper(); try (Client tc = getClient()) { @@ -175,7 +199,11 @@ public void testBulkShards() throws Exception { @Test public void testCreateIndex() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); RestHelper rh = nonSslRestHelper(); HttpResponse res; @@ -215,7 +243,11 @@ public void testCreateIndex() throws Exception { @Test public void testFilteredAlias() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); try (Client tc = getClient()) { @@ -271,7 +303,11 @@ public void testFilteredAlias() throws Exception { @Test public void testIndexTypeEvaluation() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); try (Client tc = getClient()) { tc.index( @@ -343,7 +379,11 @@ public void testIndexTypeEvaluation() throws Exception { @Test public void testIndices() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); try (Client tc = getClient()) { tc.index(new IndexRequest("nopermindex").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"content\":1}", XContentType.JSON)) @@ -541,8 +581,10 @@ public void testIndices() throws Exception { @Test public void testAliases() throws Exception { - final Settings settings = Settings.builder().put(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH").build(); - + final Settings settings = Settings.builder() + .put(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH") + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(); setup(settings); try (Client tc = getClient()) { @@ -661,7 +703,11 @@ public void testAliases() throws Exception { @Test public void testIndexResolveInvalidIndexName() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); final RestHelper rh = nonSslRestHelper(); // invalid_index_name_exception should be thrown and responded when invalid index name is mentioned in requests. @@ -676,7 +722,11 @@ public void testIndexResolveInvalidIndexName() throws Exception { @Test public void testCCSIndexResolve() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); final RestHelper rh = nonSslRestHelper(); try (Client tc = getClient()) { @@ -697,7 +747,11 @@ public void testCCSIndexResolve() throws Exception { @Ignore public void testCCSIndexResolve2() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); final RestHelper rh = nonSslRestHelper(); try (Client tc = getClient()) { @@ -763,7 +817,9 @@ public void testIndexResolveIgnoreUnavailable() throws Exception { setup( Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_respect_indices_options.yml").setSecurityRoles("roles_bs.yml"), - Settings.EMPTY, + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(), true ); final RestHelper rh = nonSslRestHelper(); @@ -789,7 +845,14 @@ public void testIndexResolveIgnoreUnavailable() throws Exception { @Test public void testIndexResolveIndicesAlias() throws Exception { - setup(Settings.EMPTY, new DynamicSecurityConfig(), Settings.EMPTY, true); + setup( + Settings.EMPTY, + new DynamicSecurityConfig(), + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(), + true + ); final RestHelper rh = nonSslRestHelper(); try (Client tc = getClient()) { @@ -821,7 +884,14 @@ public void testIndexResolveIndicesAlias() throws Exception { @Test public void testIndexResolveMinus() throws Exception { - setup(Settings.EMPTY, new DynamicSecurityConfig(), Settings.EMPTY, true); + setup( + Settings.EMPTY, + new DynamicSecurityConfig(), + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(), + true + ); final RestHelper rh = nonSslRestHelper(); try (Client tc = getClient()) { diff --git a/src/test/java/org/opensearch/security/IndexTemplateClusterPermissionsCheckTest.java b/src/test/java/org/opensearch/security/IndexTemplateClusterPermissionsCheckTest.java index 72a5c2006c..adad074a50 100644 --- a/src/test/java/org/opensearch/security/IndexTemplateClusterPermissionsCheckTest.java +++ b/src/test/java/org/opensearch/security/IndexTemplateClusterPermissionsCheckTest.java @@ -11,10 +11,17 @@ package org.opensearch.security; +import java.util.Arrays; +import java.util.Collection; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import org.apache.http.HttpStatus; import org.junit.Before; import org.junit.Test; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.test.SingleClusterTest; import org.opensearch.security.test.helper.rest.RestHelper; import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; @@ -23,6 +30,19 @@ import static org.hamcrest.Matchers.is; public class IndexTemplateClusterPermissionsCheckTest extends SingleClusterTest { + private boolean useOldPrivilegeEvaluationImplementation; + + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + + public IndexTemplateClusterPermissionsCheckTest( + @Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation + ) { + this.useOldPrivilegeEvaluationImplementation = useOldPrivilegeEvaluationImplementation; + } + private RestHelper rh; final static String indexTemplateBody = @@ -34,7 +54,11 @@ private String getFailureResponseReason(String user) { @Before public void setupRestHelper() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); rh = nonSslRestHelper(); } diff --git a/src/test/java/org/opensearch/security/InitializationIntegrationTests.java b/src/test/java/org/opensearch/security/InitializationIntegrationTests.java index d0bcb317f0..2470a80cfd 100644 --- a/src/test/java/org/opensearch/security/InitializationIntegrationTests.java +++ b/src/test/java/org/opensearch/security/InitializationIntegrationTests.java @@ -27,8 +27,12 @@ package org.opensearch.security; import java.io.File; +import java.util.Arrays; +import java.util.Collection; import java.util.Iterator; +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import com.fasterxml.jackson.databind.JsonNode; import org.apache.http.Header; import org.apache.http.HttpStatus; @@ -50,6 +54,7 @@ import org.opensearch.security.action.configupdate.ConfigUpdateAction; import org.opensearch.security.action.configupdate.ConfigUpdateRequest; import org.opensearch.security.action.configupdate.ConfigUpdateResponse; +import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.ssl.util.SSLConfigConstants; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.test.DynamicSecurityConfig; @@ -64,6 +69,19 @@ public class InitializationIntegrationTests extends SingleClusterTest { + private boolean useOldPrivilegeEvaluationImplementation; + + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + + public InitializationIntegrationTests( + @Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation + ) { + this.useOldPrivilegeEvaluationImplementation = useOldPrivilegeEvaluationImplementation; + } + @Test public void testEnsureInitViaRestDoesWork() throws Exception { @@ -72,6 +90,7 @@ public void testEnsureInitViaRestDoesWork() throws Exception { .put("plugins.security.ssl.http.enabled", true) .put("plugins.security.ssl.http.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("node-0-keystore.jks")) .put("plugins.security.ssl.http.truststore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("truststore.jks")) + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) .build(); setup(Settings.EMPTY, null, settings, false); final RestHelper rh = restHelper(); // ssl resthelper @@ -107,6 +126,7 @@ public void testInitWithInjectedUser() throws Exception { final Settings settings = Settings.builder() .putList("path.repo", repositoryPath.getRoot().getAbsolutePath()) .put("plugins.security.unsupported.inject_user.enabled", true) + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) .build(); setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_disable_all.yml"), settings, true); @@ -130,6 +150,7 @@ public void testWhoAmI() throws Exception { .put("plugins.security.ssl.http.enabled", true) .put("plugins.security.ssl.http.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("node-0-keystore.jks")) .put("plugins.security.ssl.http.truststore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("truststore.jks")) + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) .build(); setup( Settings.EMPTY, @@ -152,7 +173,11 @@ public void testWhoAmI() throws Exception { @Test public void testConfigHotReload() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); RestHelper rh = nonSslRestHelper(); Header spock = encodeBasicHeader("spock", "spock"); @@ -243,7 +268,10 @@ public void testConfigHotReload() throws Exception { @Test public void testDefaultConfig() throws Exception { - final Settings settings = Settings.builder().put(ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, true).build(); + final Settings settings = Settings.builder() + .put(ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, true) + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(); setup(Settings.EMPTY, null, settings, false); RestHelper rh = nonSslRestHelper(); Thread.sleep(10000); @@ -259,7 +287,10 @@ public void testInvalidDefaultConfig() throws Exception { final String defaultInitDirectory = ClusterHelper.updateDefaultDirectory( new File(TEST_RESOURCE_RELATIVE_PATH + "invalid_config").getAbsolutePath() ); - final Settings settings = Settings.builder().put(ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, true).build(); + final Settings settings = Settings.builder() + .put(ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, true) + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(); setup(Settings.EMPTY, null, settings, false); RestHelper rh = nonSslRestHelper(); Thread.sleep(10000); @@ -275,7 +306,10 @@ public void testInvalidDefaultConfig() throws Exception { @Test public void testDisabled() throws Exception { - final Settings settings = Settings.builder().put("plugins.security.disabled", true).build(); + final Settings settings = Settings.builder() + .put("plugins.security.disabled", true) + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(); setup(Settings.EMPTY, null, settings, false); RestHelper rh = nonSslRestHelper(); @@ -287,7 +321,14 @@ public void testDisabled() throws Exception { @Test public void testDiscoveryWithoutInitialization() throws Exception { - setup(Settings.EMPTY, null, Settings.EMPTY, false); + setup( + Settings.EMPTY, + null, + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(), + false + ); assertThat( clusterInfo.numNodes, is( diff --git a/src/test/java/org/opensearch/security/IntegrationTests.java b/src/test/java/org/opensearch/security/IntegrationTests.java index 03a5dd3092..b34de787fc 100644 --- a/src/test/java/org/opensearch/security/IntegrationTests.java +++ b/src/test/java/org/opensearch/security/IntegrationTests.java @@ -26,8 +26,12 @@ package org.opensearch.security; +import java.util.Arrays; +import java.util.Collection; import java.util.TreeSet; +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import com.fasterxml.jackson.databind.JsonNode; import org.apache.http.HttpStatus; import org.apache.http.message.BasicHeader; @@ -48,6 +52,7 @@ import org.opensearch.security.action.configupdate.ConfigUpdateRequest; import org.opensearch.security.action.configupdate.ConfigUpdateResponse; import org.opensearch.security.http.HTTPClientCertAuthenticator; +import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.ssl.util.SSLConfigConstants; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.test.DynamicSecurityConfig; @@ -63,11 +68,22 @@ import static org.opensearch.security.DefaultObjectMapper.readTree; public class IntegrationTests extends SingleClusterTest { + private boolean useOldPrivilegeEvaluationImplementation; + + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + + public IntegrationTests(@Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation) { + this.useOldPrivilegeEvaluationImplementation = useOldPrivilegeEvaluationImplementation; + } @Test public void testSearchScroll() throws Exception { final Settings settings = Settings.builder() .putList(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + ".worf", "knuddel", "nonexists") + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) .build(); setup(settings); final RestHelper rh = nonSslRestHelper(); @@ -197,6 +213,7 @@ public void testDNSpecials1() throws Exception { "EMAILADDREss=unt@xxx.com, cn=node-untspec6.example.com, OU=SSL,O=Te\\, st,L=Test, c=DE" ) .put("plugins.security.cert.oid", "1.2.3.4.5.6") + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) .build(); Settings tcSettings = Settings.builder() @@ -223,7 +240,11 @@ public void testEnsureOpenSSLAvailability() { @Test public void testMultiget() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); try (Client tc = getClient()) { tc.index( @@ -261,6 +282,7 @@ public void testRestImpersonation() throws Exception { final Settings settings = Settings.builder() .putList(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + ".spock", "knuddel", "userwhonotexists") + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) .build(); setup(settings); @@ -305,7 +327,11 @@ public void testRestImpersonation() throws Exception { @Test public void testSingle() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); try (Client tc = getClient()) { tc.index( @@ -336,7 +362,11 @@ public void testSingle() throws Exception { @Test public void testSpecialUsernames() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); RestHelper rh = nonSslRestHelper(); assertThat(rh.executeGetRequest("", encodeBasicHeader("bug.88", "nagilum")).getStatusCode(), is(HttpStatus.SC_OK)); @@ -349,7 +379,14 @@ public void testSpecialUsernames() throws Exception { @Test public void testXff() throws Exception { - setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_xff.yml"), Settings.EMPTY, true); + setup( + Settings.EMPTY, + new DynamicSecurityConfig().setConfig("config_xff.yml"), + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(), + true + ); RestHelper rh = nonSslRestHelper(); HttpResponse resc = rh.executeGetRequest( "_opendistro/_security/authinfo", @@ -363,7 +400,14 @@ public void testXff() throws Exception { @Test public void testRegexExcludes() throws Exception { - setup(Settings.EMPTY, new DynamicSecurityConfig(), Settings.EMPTY); + setup( + Settings.EMPTY, + new DynamicSecurityConfig(), + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); try (Client tc = getClient()) { tc.index(new IndexRequest("indexa").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("{\"indexa\":1}", XContentType.JSON)) @@ -400,7 +444,13 @@ public void testRegexExcludes() throws Exception { @Test public void testMultiRoleSpan() throws Exception { - setup(); + setup( + Settings.EMPTY, + new DynamicSecurityConfig().setConfig("config_multirolespan.yml"), + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); final RestHelper rh = nonSslRestHelper(); try (Client tc = getClient()) { @@ -411,24 +461,8 @@ public void testMultiRoleSpan() throws Exception { } HttpResponse res = rh.executeGetRequest("/mindex_1,mindex_2/_search", encodeBasicHeader("mindex12", "nagilum")); - assertThat(res.getStatusCode(), is(HttpStatus.SC_FORBIDDEN)); - Assert.assertFalse(res.getBody().contains("\"content\":1")); - Assert.assertFalse(res.getBody().contains("\"content\":2")); - - try (Client tc = getClient()) { - tc.index( - new IndexRequest(".opendistro_security").id("config") - .setRefreshPolicy(RefreshPolicy.IMMEDIATE) - .source("config", FileHelper.readYamlContent("config_multirolespan.yml")) - ).actionGet(); - - ConfigUpdateResponse cur = tc.execute(ConfigUpdateAction.INSTANCE, new ConfigUpdateRequest(new String[] { "config" })) - .actionGet(); - assertThat(cur.getNodes().size(), is(clusterInfo.numNodes)); - } - - res = rh.executeGetRequest("/mindex_1,mindex_2/_search", encodeBasicHeader("mindex12", "nagilum")); assertThat(res.getStatusCode(), is(HttpStatus.SC_OK)); + Assert.assertEquals(HttpStatus.SC_OK, res.getStatusCode()); Assert.assertTrue(res.getBody().contains("\"content\":1")); Assert.assertTrue(res.getBody().contains("\"content\":2")); @@ -437,7 +471,13 @@ public void testMultiRoleSpan() throws Exception { @Test public void testMultiRoleSpan2() throws Exception { - setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_multirolespan.yml"), Settings.EMPTY); + setup( + Settings.EMPTY, + new DynamicSecurityConfig().setConfig("config_multirolespan.yml"), + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); final RestHelper rh = nonSslRestHelper(); try (Client tc = getClient()) { @@ -465,7 +505,11 @@ public void testMultiRoleSpan2() throws Exception { @Test public void testSecurityUnderscore() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); final RestHelper rh = nonSslRestHelper(); HttpResponse res = rh.executePostRequest( @@ -486,7 +530,13 @@ public void testSecurityUnderscore() throws Exception { @Test public void testDeleteByQueryDnfof() throws Exception { - setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_dnfof.yml"), Settings.EMPTY); + setup( + Settings.EMPTY, + new DynamicSecurityConfig().setConfig("config_dnfof.yml"), + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); try (Client tc = getClient()) { for (int i = 0; i < 3; i++) { @@ -514,7 +564,10 @@ public void testDeleteByQueryDnfof() throws Exception { @Test public void testUpdate() throws Exception { - final Settings settings = Settings.builder().put(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH").build(); + final Settings settings = Settings.builder() + .put(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH") + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(); setup(settings); final RestHelper rh = nonSslRestHelper(); @@ -535,7 +588,10 @@ public void testUpdate() throws Exception { @Test public void testDnfof() throws Exception { - final Settings settings = Settings.builder().put(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH").build(); + final Settings settings = Settings.builder() + .put(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH") + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(); setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_dnfof.yml"), settings); final RestHelper rh = nonSslRestHelper(); @@ -782,7 +838,10 @@ public void testDnfof() throws Exception { @Test public void testNoDnfof() throws Exception { - final Settings settings = Settings.builder().put(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH").build(); + final Settings settings = Settings.builder() + .put(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, "BOTH") + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(); setup(Settings.EMPTY, new DynamicSecurityConfig(), settings); final RestHelper rh = nonSslRestHelper(); @@ -1007,7 +1066,11 @@ public void testNoDnfof() throws Exception { @Test public void testSecurityIndexSecurity() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); final RestHelper rh = nonSslRestHelper(); HttpResponse res = rh.executePutRequest( @@ -1075,7 +1138,13 @@ public void testSecurityIndexSecurity() throws Exception { @Test public void testMonitorHealth() throws Exception { - setup(Settings.EMPTY, new DynamicSecurityConfig(), Settings.EMPTY); + setup( + Settings.EMPTY, + new DynamicSecurityConfig(), + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); RestHelper rh = nonSslRestHelper(); assertThat(rh.executeGetRequest("_cat/health", encodeBasicHeader("picard", "picard")).getStatusCode(), is(HttpStatus.SC_OK)); diff --git a/src/test/java/org/opensearch/security/PitIntegrationTests.java b/src/test/java/org/opensearch/security/PitIntegrationTests.java index 11c9ba8ddd..081950cb98 100644 --- a/src/test/java/org/opensearch/security/PitIntegrationTests.java +++ b/src/test/java/org/opensearch/security/PitIntegrationTests.java @@ -11,8 +11,12 @@ package org.opensearch.security; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import org.apache.http.HttpStatus; import org.junit.Test; @@ -21,7 +25,9 @@ import org.opensearch.action.index.IndexRequest; import org.opensearch.action.support.WriteRequest; import org.opensearch.client.Client; +import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentType; +import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.test.SingleClusterTest; import org.opensearch.security.test.helper.rest.RestHelper; @@ -32,10 +38,24 @@ * Integration tests to test point in time APIs permission model */ public class PitIntegrationTests extends SingleClusterTest { + private boolean useOldPrivilegeEvaluationImplementation; + + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + + public PitIntegrationTests(@Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation) { + this.useOldPrivilegeEvaluationImplementation = useOldPrivilegeEvaluationImplementation; + } @Test public void testPitExplicitAPIAccess() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); RestHelper rh = nonSslRestHelper(); try (Client tc = getClient()) { // create alias @@ -116,7 +136,11 @@ public void testPitExplicitAPIAccess() throws Exception { @Test public void testPitAllAPIAccess() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); RestHelper rh = nonSslRestHelper(); // Create two indices @@ -187,7 +211,11 @@ public void testPitAllAPIAccess() throws Exception { @Test public void testDataStreamWithPits() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); RestHelper rh = nonSslRestHelper(); String indexTemplate = "{\"index_patterns\": [ \"my-data-stream*\" ], \"data_stream\": { }, \"priority\": 200, " + "\"template\": {\"settings\": { } } }"; diff --git a/src/test/java/org/opensearch/security/PrivilegesEvaluationTest.java b/src/test/java/org/opensearch/security/PrivilegesEvaluationTest.java index d52853c05b..f6b3d095cc 100644 --- a/src/test/java/org/opensearch/security/PrivilegesEvaluationTest.java +++ b/src/test/java/org/opensearch/security/PrivilegesEvaluationTest.java @@ -11,6 +11,11 @@ package org.opensearch.security; +import java.util.Arrays; +import java.util.Collection; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import com.google.common.collect.ImmutableMap; import org.junit.Test; @@ -18,7 +23,9 @@ import org.opensearch.action.index.IndexRequest; import org.opensearch.action.support.WriteRequest.RefreshPolicy; import org.opensearch.client.Client; +import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentType; +import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.test.SingleClusterTest; import org.opensearch.security.test.helper.rest.RestHelper; @@ -26,10 +33,25 @@ import static org.hamcrest.Matchers.is; public class PrivilegesEvaluationTest extends SingleClusterTest { + private boolean useOldPrivilegeEvaluationImplementation; + + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + + public PrivilegesEvaluationTest(@Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation) { + this.useOldPrivilegeEvaluationImplementation = useOldPrivilegeEvaluationImplementation; + } + @Test public void resolveTestHidden() throws Exception { - setup(); + setup( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); try (Client client = getClient()) { diff --git a/src/test/java/org/opensearch/security/ResolveAPITests.java b/src/test/java/org/opensearch/security/ResolveAPITests.java index 9d1c77341e..f38762787f 100644 --- a/src/test/java/org/opensearch/security/ResolveAPITests.java +++ b/src/test/java/org/opensearch/security/ResolveAPITests.java @@ -15,6 +15,11 @@ package org.opensearch.security; +import java.util.Arrays; +import java.util.Collection; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -27,6 +32,7 @@ import org.opensearch.client.Client; import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentType; +import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.test.DynamicSecurityConfig; import org.opensearch.security.test.SingleClusterTest; import org.opensearch.security.test.helper.rest.RestHelper; @@ -35,14 +41,25 @@ import static org.hamcrest.Matchers.is; public class ResolveAPITests extends SingleClusterTest { + private boolean useOldPrivilegeEvaluationImplementation; + + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + + public ResolveAPITests(@Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation) { + this.useOldPrivilegeEvaluationImplementation = useOldPrivilegeEvaluationImplementation; + } protected final Logger log = LogManager.getLogger(this.getClass()); @Test public void testResolveDnfofFalse() throws Exception { - Settings settings = Settings.builder().build(); - + Settings settings = Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(); setup(settings); setupIndices(); @@ -97,7 +114,9 @@ public void testResolveDnfofFalse() throws Exception { @Test public void testResolveDnfofTrue() throws Exception { - final Settings settings = Settings.builder().build(); + Settings settings = Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(); setup(Settings.EMPTY, new DynamicSecurityConfig().setConfig("config_dnfof.yml"), settings); setupIndices(); diff --git a/src/test/java/org/opensearch/security/RolesInjectorIntegTest.java b/src/test/java/org/opensearch/security/RolesInjectorIntegTest.java index 2492ff2fb0..104435836f 100644 --- a/src/test/java/org/opensearch/security/RolesInjectorIntegTest.java +++ b/src/test/java/org/opensearch/security/RolesInjectorIntegTest.java @@ -17,9 +17,12 @@ import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.function.Supplier; +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import com.google.common.collect.Lists; import org.junit.Assert; import org.junit.Test; @@ -45,6 +48,7 @@ import org.opensearch.plugins.Plugin; import org.opensearch.repositories.RepositoriesService; import org.opensearch.script.ScriptService; +import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.test.AbstractSecurityUnitTest; import org.opensearch.security.test.DynamicSecurityConfig; @@ -57,6 +61,16 @@ import static org.hamcrest.Matchers.is; public class RolesInjectorIntegTest extends SingleClusterTest { + private boolean useOldPrivilegeEvaluationImplementation; + + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + + public RolesInjectorIntegTest(@Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation) { + this.useOldPrivilegeEvaluationImplementation = useOldPrivilegeEvaluationImplementation; + } public static class RolesInjectorPlugin extends Plugin implements ActionPlugin { Settings settings; @@ -88,7 +102,13 @@ public Collection createComponents( @Test public void testRolesInject() throws Exception { - setup(Settings.EMPTY, new DynamicSecurityConfig().setSecurityRoles("roles.yml"), Settings.EMPTY); + setup( + Settings.EMPTY, + new DynamicSecurityConfig().setSecurityRoles("roles.yml"), + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); assertThat( clusterInfo.numNodes, diff --git a/src/test/java/org/opensearch/security/RolesValidationIntegTest.java b/src/test/java/org/opensearch/security/RolesValidationIntegTest.java index 8dc3a45fca..01fb037ba0 100644 --- a/src/test/java/org/opensearch/security/RolesValidationIntegTest.java +++ b/src/test/java/org/opensearch/security/RolesValidationIntegTest.java @@ -13,9 +13,12 @@ import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.function.Supplier; +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import com.google.common.collect.Lists; import org.junit.Assert; import org.junit.Test; @@ -39,6 +42,7 @@ import org.opensearch.plugins.Plugin; import org.opensearch.repositories.RepositoriesService; import org.opensearch.script.ScriptService; +import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.test.AbstractSecurityUnitTest; import org.opensearch.security.test.DynamicSecurityConfig; @@ -48,6 +52,16 @@ import org.opensearch.watcher.ResourceWatcherService; public class RolesValidationIntegTest extends SingleClusterTest { + private boolean useOldPrivilegeEvaluationImplementation; + + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + + public RolesValidationIntegTest(@Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation) { + this.useOldPrivilegeEvaluationImplementation = useOldPrivilegeEvaluationImplementation; + } public static class RolesValidationPlugin extends Plugin implements ActionPlugin { Settings settings; @@ -82,7 +96,13 @@ public Collection createComponents( @Test public void testRolesValidation() throws Exception { - setup(Settings.EMPTY, new DynamicSecurityConfig().setSecurityRoles("roles.yml"), Settings.EMPTY); + setup( + Settings.EMPTY, + new DynamicSecurityConfig().setSecurityRoles("roles.yml"), + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); final Settings tcSettings = AbstractSecurityUnitTest.nodeRolesSettings(Settings.builder(), false, false) .put(minimumSecuritySettings(Settings.EMPTY).get(0)) diff --git a/src/test/java/org/opensearch/security/SnapshotRestoreTests.java b/src/test/java/org/opensearch/security/SnapshotRestoreTests.java index 0291b79577..5c5be7b199 100644 --- a/src/test/java/org/opensearch/security/SnapshotRestoreTests.java +++ b/src/test/java/org/opensearch/security/SnapshotRestoreTests.java @@ -27,8 +27,11 @@ package org.opensearch.security; import java.util.Arrays; +import java.util.Collection; import java.util.List; +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; @@ -45,6 +48,7 @@ import org.opensearch.security.action.configupdate.ConfigUpdateAction; import org.opensearch.security.action.configupdate.ConfigUpdateRequest; import org.opensearch.security.action.configupdate.ConfigUpdateResponse; +import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.test.DynamicSecurityConfig; import org.opensearch.security.test.SingleClusterTest; import org.opensearch.security.test.helper.cluster.ClusterConfiguration; @@ -56,6 +60,17 @@ import static org.hamcrest.Matchers.not; public class SnapshotRestoreTests extends SingleClusterTest { + private boolean useOldPrivilegeEvaluationImplementation; + + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + + public SnapshotRestoreTests(@Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation) { + this.useOldPrivilegeEvaluationImplementation = useOldPrivilegeEvaluationImplementation; + } + private ClusterConfiguration currentClusterConfig = ClusterConfiguration.DEFAULT; @Test @@ -65,6 +80,7 @@ public void testSnapshotEnableSecurityIndexRestore() throws Exception { .putList("path.repo", repositoryPath.getRoot().getAbsolutePath()) .put("plugins.security.check_snapshot_restore_write_privileges", false) .put("plugins.security.unsupported.restore.securityindex.enabled", true) + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) .build(); setup(settings, currentClusterConfig); @@ -858,7 +874,10 @@ public void testSnapshotRestoreSpecialIndicesPatterns() throws Exception { final List listOfIndexesToTest = Arrays.asList("foo", "bar", "baz"); - final Settings settings = Settings.builder().putList("path.repo", repositoryPath.getRoot().getAbsolutePath()).build(); + final Settings settings = Settings.builder() + .putList("path.repo", repositoryPath.getRoot().getAbsolutePath()) + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build(); setup( Settings.EMPTY, @@ -940,6 +959,7 @@ public void testNoSnapshotRestore() throws Exception { final Settings settings = Settings.builder() .putList("path.repo", repositoryPath.getRoot().getAbsolutePath()) .put("plugins.security.enable_snapshot_restore_privilege", false) + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) .build(); setup( diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RoleBasedAccessTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RoleBasedAccessTest.java index fa4096596d..60ab44567e 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/RoleBasedAccessTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RoleBasedAccessTest.java @@ -11,6 +11,11 @@ package org.opensearch.security.dlic.rest.api; +import java.util.Arrays; +import java.util.Collection; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; @@ -18,6 +23,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.XContentType; import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.support.SecurityJsonNode; import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; @@ -27,20 +33,32 @@ import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; public class RoleBasedAccessTest extends AbstractRestApiUnitTest { + private boolean useOldPrivilegeEvaluationImplementation; + + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + private final String ENDPOINT; protected String getEndpointPrefix() { return PLUGINS_PREFIX; } - public RoleBasedAccessTest() { + public RoleBasedAccessTest(@Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation) { + this.useOldPrivilegeEvaluationImplementation = useOldPrivilegeEvaluationImplementation; ENDPOINT = getEndpointPrefix() + "/api"; } @Test public void testActionGroupsApi() throws Exception { - setupWithRestRoles(); + setupWithRestRoles( + Settings.builder() + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) + .build() + ); rh.sendAdminCertificate = false; diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyRoleBasedAccessTests.java b/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyRoleBasedAccessTests.java index 329404dfe7..e891d7b49f 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyRoleBasedAccessTests.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyRoleBasedAccessTests.java @@ -11,11 +11,27 @@ package org.opensearch.security.dlic.rest.api.legacy; +import java.util.Arrays; +import java.util.Collection; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + import org.opensearch.security.dlic.rest.api.RoleBasedAccessTest; import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; public class LegacyRoleBasedAccessTests extends RoleBasedAccessTest { + + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + + public LegacyRoleBasedAccessTests(@Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation) { + super(useOldPrivilegeEvaluationImplementation); + } + @Override protected String getEndpointPrefix() { return LEGACY_OPENDISTRO_PREFIX; diff --git a/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java deleted file mode 100644 index 706f5bcecd..0000000000 --- a/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.privileges; - -import org.apache.http.Header; -import org.apache.http.HttpStatus; -import org.junit.Before; -import org.junit.Test; - -import org.opensearch.common.settings.Settings; -import org.opensearch.security.test.DynamicSecurityConfig; -import org.opensearch.security.test.SingleClusterTest; -import org.opensearch.security.test.helper.rest.RestHelper; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -public class PrivilegesEvaluatorTest extends SingleClusterTest { - private static final Header NegativeLookaheadUserHeader = encodeBasicHeader("negative_lookahead_user", "negative_lookahead_user"); - private static final Header NegatedRegexUserHeader = encodeBasicHeader("negated_regex_user", "negated_regex_user"); - - @Before - public void setupSettingsIndexPattern() throws Exception { - Settings settings = Settings.builder().build(); - setup( - Settings.EMPTY, - new DynamicSecurityConfig().setSecurityRoles("roles_index_patterns.yml") - .setSecurityInternalUsers("internal_users_index_patterns.yml") - .setSecurityRolesMapping("roles_mapping_index_patterns.yml"), - settings, - true - ); - } - - @Test - public void testNegativeLookaheadPattern() throws Exception { - - RestHelper rh = nonSslRestHelper(); - RestHelper.HttpResponse response = rh.executeGetRequest("*/_search", NegativeLookaheadUserHeader); - assertThat(response.getStatusCode(), is(HttpStatus.SC_FORBIDDEN)); - response = rh.executeGetRequest("r*/_search", NegativeLookaheadUserHeader); - assertThat(response.getStatusCode(), is(HttpStatus.SC_OK)); - } - - @Test - public void testRegexPattern() throws Exception { - RestHelper rh = nonSslRestHelper(); - RestHelper.HttpResponse response = rh.executeGetRequest("*/_search", NegatedRegexUserHeader); - assertThat(response.getStatusCode(), is(HttpStatus.SC_FORBIDDEN)); - response = rh.executeGetRequest("r*/_search", NegatedRegexUserHeader); - assertThat(response.getStatusCode(), is(HttpStatus.SC_OK)); - } -} diff --git a/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java b/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java index 37686ca3c4..3c88023bb0 100644 --- a/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java +++ b/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java @@ -15,8 +15,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; -import static org.opensearch.security.privileges.PrivilegesEvaluator.DNFOF_MATCHER; -import static org.opensearch.security.privileges.PrivilegesEvaluator.isClusterPerm; +import static org.opensearch.security.privileges.PrivilegesEvaluatorImpl.DNFOF_MATCHER; +import static org.opensearch.security.privileges.PrivilegesEvaluatorImpl.isClusterPerm; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; diff --git a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java index c374a10c24..f9e650f734 100644 --- a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java @@ -11,28 +11,33 @@ package org.opensearch.security.privileges; -import java.util.Collections; import java.util.Set; +import java.util.TreeMap; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.config.Configurator; -import org.hamcrest.Matchers; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.opensearch.OpenSearchSecurityException; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.metadata.Metadata; import org.opensearch.cluster.node.DiscoveryNode; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.auditlog.NullAuditLog; import org.opensearch.security.securityconf.ConfigModel; -import org.opensearch.security.securityconf.SecurityRoles; +import org.opensearch.security.securityconf.DynamicConfigModel; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.user.User; -import org.opensearch.threadpool.ThreadPool; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @@ -41,12 +46,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThrows; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; @@ -56,11 +56,9 @@ public class RestLayerPrivilegesEvaluatorTest { @Mock(strictness = Mock.Strictness.LENIENT) private ClusterService clusterService; @Mock - private ThreadPool threadPool; - @Mock private ConfigModel configModel; - - private RestLayerPrivilegesEvaluator privilegesEvaluator; + @Mock + private DynamicConfigModel dynamicConfigModel; private static final User TEST_USER = new User("test_user"); @@ -71,16 +69,14 @@ private void setLoggingLevel(final Level level) { @Before public void setUp() { - when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY)); - when(clusterService.localNode()).thenReturn(mock(DiscoveryNode.class, withSettings().strictness(Strictness.LENIENT))); - privilegesEvaluator = new RestLayerPrivilegesEvaluator( - clusterService, - threadPool - ); - privilegesEvaluator.onConfigModelChanged(configModel); // Defaults to the mocked config model - verify(threadPool).getThreadContext(); // Called during construction of RestLayerPrivilegesEvaluator + when(configModel.mapSecurityRoles(TEST_USER, null)).thenReturn(Set.of("test_role")); setLoggingLevel(Level.DEBUG); // Enable debug logging scenarios for verification + ClusterState clusterState = mock(ClusterState.class); + when(clusterService.state()).thenReturn(clusterState); + Metadata metadata = mock(Metadata.class); + when(clusterState.metadata()).thenReturn(metadata); + when(metadata.getIndicesLookup()).thenReturn(new TreeMap<>()); } @After @@ -89,96 +85,89 @@ public void after() { } @Test - public void testEvaluate_Initialized_Success() { + public void testEvaluate_Initialized_Success() throws Exception { String action = "action"; - SecurityRoles securityRoles = mock(SecurityRoles.class); - when(configModel.getSecurityRoles()).thenReturn(securityRoles); - when(configModel.getSecurityRoles().filter(Collections.emptySet())).thenReturn(securityRoles); - when(securityRoles.impliesClusterPermissionPermission(action)).thenReturn(false); + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - any", CType.ROLES); + + PrivilegesEvaluatorImpl privilegesEvaluator = createPrivilegesEvaluator(roles); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluatorImpl(privilegesEvaluator); - PrivilegesEvaluatorResponse response = privilegesEvaluator.evaluate(TEST_USER, Set.of(action)); + PrivilegesEvaluatorResponse response = restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", Set.of(action)); assertThat(response.isAllowed(), equalTo(false)); assertThat(response.getMissingPrivileges(), equalTo(Set.of(action))); - assertThat(response.getResolvedSecurityRoles(), Matchers.empty()); - verify(configModel, times(3)).getSecurityRoles(); } @Test public void testEvaluate_NotInitialized_NullModel_ExceptionThrown() { - // Null out the config model - privilegesEvaluator.onConfigModelChanged(null); - final OpenSearchSecurityException exception = assertThrows( - OpenSearchSecurityException.class, - () -> privilegesEvaluator.evaluate(TEST_USER, null) - ); - assertThat(exception.getMessage(), equalTo("OpenSearch Security is not initialized.")); - verify(configModel, never()).getSecurityRoles(); - } - - @Test - public void testEvaluate_NotInitialized_NoSecurityRoles_ExceptionThrown() { + PrivilegesEvaluatorImpl privilegesEvaluator = createPrivilegesEvaluator(null); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluatorImpl(privilegesEvaluator); final OpenSearchSecurityException exception = assertThrows( OpenSearchSecurityException.class, - () -> privilegesEvaluator.evaluate(TEST_USER, null) + () -> restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", null) ); assertThat(exception.getMessage(), equalTo("OpenSearch Security is not initialized.")); - verify(configModel).getSecurityRoles(); } @Test - public void testMapRoles_ReturnsMappedRoles() { - final User user = mock(User.class); - final Set mappedRoles = Collections.singleton("role1"); - when(configModel.mapSecurityRoles(any(), any())).thenReturn(mappedRoles); - - final Set result = privilegesEvaluator.mapRoles(user, null); - - assertThat(result, equalTo(mappedRoles)); - verifyNoInteractions(user); - verify(configModel).mapSecurityRoles(user, null); - } - - @Test - public void testEvaluate_Successful_NewPermission() { + public void testEvaluate_Successful_NewPermission() throws Exception { String action = "hw:greet"; - SecurityRoles securityRoles = mock(SecurityRoles.class); - when(configModel.getSecurityRoles()).thenReturn(securityRoles); - when(configModel.getSecurityRoles().filter(Collections.emptySet())).thenReturn(securityRoles); - when(securityRoles.impliesClusterPermissionPermission(action)).thenReturn(true); - - PrivilegesEvaluatorResponse response = privilegesEvaluator.evaluate(TEST_USER, Set.of(action)); - + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - hw:greet", CType.ROLES); + PrivilegesEvaluatorImpl privilegesEvaluator = createPrivilegesEvaluator(roles); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluatorImpl(privilegesEvaluator); + PrivilegesEvaluatorResponse response = restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", Set.of(action)); assertThat(response.allowed, equalTo(true)); - verify(securityRoles).impliesClusterPermissionPermission(action); } @Test - public void testEvaluate_Successful_LegacyPermission() { + public void testEvaluate_Successful_LegacyPermission() throws Exception { String action = "cluster:admin/opensearch/hw/greet"; - SecurityRoles securityRoles = mock(SecurityRoles.class); - when(configModel.getSecurityRoles()).thenReturn(securityRoles); - when(configModel.getSecurityRoles().filter(Collections.emptySet())).thenReturn(securityRoles); - when(securityRoles.impliesClusterPermissionPermission(action)).thenReturn(true); - - PrivilegesEvaluatorResponse response = privilegesEvaluator.evaluate(TEST_USER, Set.of(action)); - + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - cluster:admin/opensearch/hw/greet", CType.ROLES); + PrivilegesEvaluatorImpl privilegesEvaluator = createPrivilegesEvaluator(roles); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluatorImpl(privilegesEvaluator); + PrivilegesEvaluatorResponse response = restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", Set.of(action)); assertThat(response.allowed, equalTo(true)); - verify(securityRoles).impliesClusterPermissionPermission(action); - verify(configModel, times(3)).getSecurityRoles(); } @Test - public void testEvaluate_Unsuccessful() { + public void testEvaluate_Unsuccessful() throws Exception { String action = "action"; - SecurityRoles securityRoles = mock(SecurityRoles.class); - when(configModel.getSecurityRoles()).thenReturn(securityRoles); - when(configModel.getSecurityRoles().filter(Collections.emptySet())).thenReturn(securityRoles); - when(securityRoles.impliesClusterPermissionPermission(action)).thenReturn(false); + SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // + " cluster_permissions:\n" + // + " - other_action", CType.ROLES); + PrivilegesEvaluatorImpl privilegesEvaluator = createPrivilegesEvaluator(roles); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluatorImpl(privilegesEvaluator); + PrivilegesEvaluatorResponse response = restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", Set.of(action)); + assertThat(response.allowed, equalTo(false)); + } - PrivilegesEvaluatorResponse response = privilegesEvaluator.evaluate(TEST_USER, Set.of(action)); + PrivilegesEvaluatorImpl createPrivilegesEvaluator(SecurityDynamicConfiguration roles) { + PrivilegesEvaluatorImpl privilegesEvaluator = new PrivilegesEvaluatorImpl( + clusterService, + () -> clusterService.state(), + null, + new ThreadContext(Settings.EMPTY), + null, + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), + new NullAuditLog(), + Settings.EMPTY, + null, + null, + null, + null + ); + privilegesEvaluator.onConfigModelChanged(configModel); // Defaults to the mocked config model + privilegesEvaluator.onDynamicConfigModelChanged(dynamicConfigModel); - assertThat(response.allowed, equalTo(false)); - verify(securityRoles).impliesClusterPermissionPermission(action); + if (roles != null) { + privilegesEvaluator.updateConfiguration(SecurityDynamicConfiguration.empty(CType.ACTIONGROUPS), roles); + } + return privilegesEvaluator; } } diff --git a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java index 58e811fa24..878033fd5c 100644 --- a/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/SystemIndexAccessEvaluatorTest.java @@ -11,13 +11,13 @@ package org.opensearch.security.privileges; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import java.util.Arrays; import java.util.List; import java.util.Set; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.fasterxml.jackson.core.JsonProcessingException; import org.apache.logging.log4j.Logger; import org.junit.After; import org.junit.Test; @@ -27,6 +27,7 @@ import org.opensearch.action.get.MultiGetRequest; import org.opensearch.action.search.SearchRequest; import org.opensearch.action.support.IndicesOptions; +import org.opensearch.cluster.metadata.IndexAbstraction; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.settings.Settings; @@ -34,10 +35,13 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; -import org.opensearch.security.securityconf.ConfigModelV7; -import org.opensearch.security.securityconf.SecurityRoles; +import org.opensearch.security.securityconf.FlattenedActionGroups; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; +import org.opensearch.security.util.MockIndexMetadataBuilder; import org.opensearch.tasks.Task; import org.mockito.Mock; @@ -46,9 +50,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.opensearch.security.support.ConfigConstants.SYSTEM_INDEX_PERMISSION; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -81,12 +83,12 @@ public class SystemIndexAccessEvaluatorTest { private static final String TEST_INDEX = ".test"; private static final String SECURITY_INDEX = ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX; - @Mock - SecurityRoles securityRoles; + ImmutableMap indexMetadata = MockIndexMetadataBuilder.indices(TEST_INDEX, TEST_SYSTEM_INDEX, SECURITY_INDEX) + .build(); User user; - IndexNameExpressionResolver indexNameExpressionResolver; + ActionPrivileges actionPrivileges; private ThreadContext createThreadContext() { return new ThreadContext(Settings.EMPTY); @@ -105,29 +107,29 @@ public void setup( ThreadContext threadContext = createThreadContext(); indexNameExpressionResolver = createIndexNameExpressionResolver(threadContext); - // create a security role - ConfigModelV7.IndexPattern ip = spy(new ConfigModelV7.IndexPattern(index)); - ConfigModelV7.SecurityRole.Builder _securityRole = new ConfigModelV7.SecurityRole.Builder("role_a"); - ip.addPerm(createIndexPatternWithSystemIndexPermission ? Set.of("*", SYSTEM_INDEX_PERMISSION) : Set.of("*")); - _securityRole.addIndexPattern(ip); - _securityRole.addClusterPerms(List.of("*")); - ConfigModelV7.SecurityRole secRole = _securityRole.build(); - try { - // create an instance of Security Role - Constructor constructor = ConfigModelV7.SecurityRoles.class.getDeclaredConstructor(int.class); - constructor.setAccessible(true); - securityRoles = constructor.newInstance(1); - - // add security role to Security Roles - Method addSecurityRoleMethod = ConfigModelV7.SecurityRoles.class.getDeclaredMethod( - "addSecurityRole", - ConfigModelV7.SecurityRole.class + SecurityDynamicConfiguration rolesConfig = SecurityDynamicConfiguration.fromMap( + ImmutableMap.of( + "role_a", + ImmutableMap.of( + "index_permissions", + Arrays.asList( + ImmutableMap.of( + "index_patterns", + Arrays.asList(index), + "allowed_actions", + createIndexPatternWithSystemIndexPermission ? Set.of("*", SYSTEM_INDEX_PERMISSION) : Set.of("*") + ) + ), + "cluster_permissions", + Arrays.asList("*") + ) + ), + CType.ROLES ); - addSecurityRoleMethod.setAccessible(true); - addSecurityRoleMethod.invoke(securityRoles, secRole); - } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { + this.actionPrivileges = new ActionPrivileges(rolesConfig, FlattenedActionGroups.EMPTY, () -> indexMetadata, Settings.EMPTY); + } catch (JsonProcessingException e) { throw new RuntimeException(e); } @@ -150,8 +152,19 @@ public void setup( when(log.isDebugEnabled()).thenReturn(true); when(log.isInfoEnabled()).thenReturn(true); + } - doReturn(ImmutableSet.of(index)).when(ip).getResolvedIndexPattern(user, indexNameExpressionResolver, cs, true, false); + PrivilegesEvaluationContext ctx(String action) { + return new PrivilegesEvaluationContext( + user, + ImmutableSet.of("role_a"), + action, + request, + null, + null, + indexNameExpressionResolver, + null + ); } @After @@ -171,10 +184,9 @@ public void testUnprotectedActionOnRegularIndex_systemIndexDisabled() { UNPROTECTED_ACTION, resolved, presponse, - securityRoles, - user, - indexNameExpressionResolver, - cs + ctx(UNPROTECTED_ACTION), + actionPrivileges, + user ); verifyNoInteractions(presponse); assertThat(response, is(presponse)); @@ -193,10 +205,9 @@ public void testUnprotectedActionOnRegularIndex_systemIndexPermissionDisabled() UNPROTECTED_ACTION, resolved, presponse, - securityRoles, - user, - indexNameExpressionResolver, - cs + ctx(UNPROTECTED_ACTION), + actionPrivileges, + user ); verifyNoInteractions(presponse); assertThat(response, is(presponse)); @@ -214,10 +225,9 @@ public void testUnprotectedActionOnRegularIndex_systemIndexPermissionEnabled() { UNPROTECTED_ACTION, resolved, presponse, - securityRoles, - user, - indexNameExpressionResolver, - cs + ctx(UNPROTECTED_ACTION), + actionPrivileges, + user ); verifyNoInteractions(presponse); assertThat(response, is(presponse)); @@ -235,10 +245,9 @@ public void testUnprotectedActionOnSystemIndex_systemIndexDisabled() { UNPROTECTED_ACTION, resolved, presponse, - securityRoles, - user, - indexNameExpressionResolver, - cs + ctx(UNPROTECTED_ACTION), + actionPrivileges, + user ); verifyNoInteractions(presponse); assertThat(response, is(presponse)); @@ -256,10 +265,9 @@ public void testUnprotectedActionOnSystemIndex_systemIndexPermissionDisabled() { UNPROTECTED_ACTION, resolved, presponse, - securityRoles, - user, - indexNameExpressionResolver, - cs + ctx(UNPROTECTED_ACTION), + actionPrivileges, + user ); verifyNoInteractions(presponse); assertThat(response, is(presponse)); @@ -277,17 +285,21 @@ public void testUnprotectedActionOnSystemIndex_systemIndexPermissionEnabled_With UNPROTECTED_ACTION, resolved, presponse, - securityRoles, - user, - indexNameExpressionResolver, - cs + ctx(UNPROTECTED_ACTION), + actionPrivileges, + user ); verify(presponse).markComplete(); assertThat(response, is(presponse)); verify(auditLog).logSecurityIndexAttempt(request, UNPROTECTED_ACTION, null); verify(log).isInfoEnabled(); - verify(log).info("No {} permission for user roles {} to System Indices {}", UNPROTECTED_ACTION, securityRoles, TEST_SYSTEM_INDEX); + verify(log).info( + "No {} permission for user roles {} to System Indices {}", + UNPROTECTED_ACTION, + user.getSecurityRoles(), + TEST_SYSTEM_INDEX + ); } @Test @@ -302,10 +314,9 @@ public void testUnprotectedActionOnSystemIndex_systemIndexPermissionEnabled_With UNPROTECTED_ACTION, resolved, presponse, - securityRoles, - user, - indexNameExpressionResolver, - cs + ctx(UNPROTECTED_ACTION), + actionPrivileges, + user ); assertThat(response, is(presponse)); // unprotected action is not allowed on a system index @@ -321,29 +332,9 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexDisabled() { final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); // Action - evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); - evaluator.evaluate( - searchRequest, - null, - UNPROTECTED_ACTION, - resolved, - presponse, - securityRoles, - user, - indexNameExpressionResolver, - cs - ); - evaluator.evaluate( - realtimeRequest, - null, - UNPROTECTED_ACTION, - resolved, - presponse, - securityRoles, - user, - indexNameExpressionResolver, - cs - ); + evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); + evaluator.evaluate(searchRequest, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); + evaluator.evaluate(realtimeRequest, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); verifyNoInteractions(presponse); } @@ -357,29 +348,9 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionDisable final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); // Action - evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); - evaluator.evaluate( - searchRequest, - null, - UNPROTECTED_ACTION, - resolved, - presponse, - securityRoles, - user, - indexNameExpressionResolver, - cs - ); - evaluator.evaluate( - realtimeRequest, - null, - UNPROTECTED_ACTION, - resolved, - presponse, - securityRoles, - user, - indexNameExpressionResolver, - cs - ); + evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); + evaluator.evaluate(searchRequest, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); + evaluator.evaluate(realtimeRequest, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); verify(searchRequest).requestCache(Boolean.FALSE); verify(realtimeRequest).realtime(Boolean.FALSE); @@ -398,29 +369,9 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); // Action - evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); - evaluator.evaluate( - searchRequest, - null, - UNPROTECTED_ACTION, - resolved, - presponse, - securityRoles, - user, - indexNameExpressionResolver, - cs - ); - evaluator.evaluate( - realtimeRequest, - null, - UNPROTECTED_ACTION, - resolved, - presponse, - securityRoles, - user, - indexNameExpressionResolver, - cs - ); + evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); + evaluator.evaluate(searchRequest, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); + evaluator.evaluate(realtimeRequest, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); verify(searchRequest).requestCache(Boolean.FALSE); verify(realtimeRequest).realtime(Boolean.FALSE); @@ -437,7 +388,7 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled verify(log, times(3)).info( "No {} permission for user roles {} to System Indices {}", UNPROTECTED_ACTION, - securityRoles, + user.getSecurityRoles(), TEST_SYSTEM_INDEX ); verify(log).debug("Disable search request cache for this request"); @@ -453,29 +404,9 @@ public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); // Action - evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); - evaluator.evaluate( - searchRequest, - null, - UNPROTECTED_ACTION, - resolved, - presponse, - securityRoles, - user, - indexNameExpressionResolver, - cs - ); - evaluator.evaluate( - realtimeRequest, - null, - UNPROTECTED_ACTION, - resolved, - presponse, - securityRoles, - user, - indexNameExpressionResolver, - cs - ); + evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); + evaluator.evaluate(searchRequest, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); + evaluator.evaluate(realtimeRequest, null, UNPROTECTED_ACTION, resolved, presponse, ctx(UNPROTECTED_ACTION), actionPrivileges, user); verify(searchRequest).requestCache(Boolean.FALSE); verify(realtimeRequest).realtime(Boolean.FALSE); @@ -491,7 +422,7 @@ public void testProtectedActionLocalAll_systemIndexDisabled() { final Resolved resolved = Resolved._LOCAL_ALL; // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); @@ -505,7 +436,7 @@ public void testProtectedActionLocalAll_systemIndexPermissionDisabled() { final Resolved resolved = Resolved._LOCAL_ALL; // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); @@ -519,7 +450,7 @@ public void testProtectedActionLocalAll_systemIndexPermissionEnabled() { final Resolved resolved = Resolved._LOCAL_ALL; // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); @@ -533,7 +464,7 @@ public void testProtectedActionOnRegularIndex_systemIndexDisabled() { final Resolved resolved = createResolved(TEST_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); assertThat(presponse.allowed, is(false)); } @@ -544,7 +475,7 @@ public void testProtectedActionOnRegularIndex_systemIndexPermissionDisabled() { final Resolved resolved = createResolved(TEST_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); assertThat(presponse.allowed, is(false)); } @@ -555,7 +486,7 @@ public void testProtectedActionOnRegularIndex_systemIndexPermissionEnabled() { final Resolved resolved = createResolved(TEST_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); assertThat(presponse.allowed, is(false)); } @@ -566,7 +497,7 @@ public void testProtectedActionOnSystemIndex_systemIndexDisabled() { final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); assertThat(presponse.allowed, is(false)); } @@ -577,7 +508,7 @@ public void testProtectedActionOnSystemIndex_systemIndexPermissionDisabled() { final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); @@ -591,13 +522,18 @@ public void testProtectedActionOnSystemIndex_systemIndexPermissionEnabled_withou final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); verify(presponse).markComplete(); verify(log).isInfoEnabled(); - verify(log).info("No {} permission for user roles {} to System Indices {}", PROTECTED_ACTION, securityRoles, TEST_SYSTEM_INDEX); + verify(log).info( + "No {} permission for user roles {} to System Indices {}", + PROTECTED_ACTION, + user.getSecurityRoles(), + TEST_SYSTEM_INDEX + ); } @Test @@ -607,7 +543,7 @@ public void testProtectedActionOnSystemIndex_systemIndexPermissionEnabled_withSy final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); assertThat(presponse.allowed, is(false)); } @@ -618,7 +554,7 @@ public void testProtectedActionOnProtectedSystemIndex_systemIndexDisabled() { final Resolved resolved = createResolved(SECURITY_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); @@ -633,7 +569,7 @@ public void testProtectedActionOnProtectedSystemIndex_systemIndexPermissionDisab final Resolved resolved = createResolved(SECURITY_INDEX); // Action - evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, ctx(PROTECTED_ACTION), actionPrivileges, user); verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); assertThat(presponse.allowed, is(false)); @@ -668,14 +604,19 @@ private void testSecurityIndexAccess(String action) { final Resolved resolved = createResolved(SECURITY_INDEX); // Action - evaluator.evaluate(request, task, action, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate(request, task, action, resolved, presponse, ctx(action), actionPrivileges, user); verify(auditLog).logSecurityIndexAttempt(request, action, task); assertThat(presponse.allowed, is(false)); verify(presponse).markComplete(); verify(log).isInfoEnabled(); - verify(log).info("{} not permitted for a regular user {} on protected system indices {}", action, securityRoles, SECURITY_INDEX); + verify(log).info( + "{} not permitted for a regular user {} on protected system indices {}", + action, + user.getSecurityRoles(), + SECURITY_INDEX + ); } private Resolved createResolved(final String... indexes) { diff --git a/src/test/java/org/opensearch/security/privileges/legacy/RestLayerPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/legacy/RestLayerPrivilegesEvaluatorTest.java new file mode 100644 index 0000000000..053643daeb --- /dev/null +++ b/src/test/java/org/opensearch/security/privileges/legacy/RestLayerPrivilegesEvaluatorTest.java @@ -0,0 +1,183 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.privileges.legacy; + +import java.util.Collections; +import java.util.Set; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.config.Configurator; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.cluster.node.DiscoveryNode; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.securityconf.SecurityRoles; +import org.opensearch.security.user.User; +import org.opensearch.threadpool.ThreadPool; + +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.quality.Strictness; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +@RunWith(MockitoJUnitRunner.class) +public class RestLayerPrivilegesEvaluatorTest { + + @Mock(strictness = Mock.Strictness.LENIENT) + private ClusterService clusterService; + @Mock + private ThreadPool threadPool; + @Mock + private ConfigModel configModel; + + private RestLayerPrivilegesEvaluatorImpl privilegesEvaluator; + + private static final User TEST_USER = new User("test_user"); + + private void setLoggingLevel(final Level level) { + final Logger restLayerPrivilegesEvaluatorLogger = LogManager.getLogger(RestLayerPrivilegesEvaluatorImpl.class); + Configurator.setLevel(restLayerPrivilegesEvaluatorLogger, level); + } + + @Before + public void setUp() { + when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY)); + + when(clusterService.localNode()).thenReturn(mock(DiscoveryNode.class, withSettings().strictness(Strictness.LENIENT))); + privilegesEvaluator = new RestLayerPrivilegesEvaluatorImpl( + clusterService, + threadPool + ); + privilegesEvaluator.onConfigModelChanged(configModel); // Defaults to the mocked config model + verify(threadPool).getThreadContext(); // Called during construction of RestLayerPrivilegesEvaluator + setLoggingLevel(Level.DEBUG); // Enable debug logging scenarios for verification + } + + @After + public void after() { + setLoggingLevel(Level.INFO); + } + + @Test + public void testEvaluate_Initialized_Success() { + String action = "action"; + SecurityRoles securityRoles = mock(SecurityRoles.class); + when(configModel.getSecurityRoles()).thenReturn(securityRoles); + when(configModel.getSecurityRoles().filter(Collections.emptySet())).thenReturn(securityRoles); + when(securityRoles.impliesClusterPermissionPermission(action)).thenReturn(false); + + PrivilegesEvaluatorResponse response = privilegesEvaluator.evaluate(TEST_USER, "", Set.of(action)); + + assertThat(response.isAllowed(), equalTo(false)); + assertThat(response.getMissingPrivileges(), equalTo(Set.of(action))); + verify(configModel, times(3)).getSecurityRoles(); + } + + @Test + public void testEvaluate_NotInitialized_NullModel_ExceptionThrown() { + // Null out the config model + privilegesEvaluator.onConfigModelChanged(null); + final OpenSearchSecurityException exception = assertThrows( + OpenSearchSecurityException.class, + () -> privilegesEvaluator.evaluate(TEST_USER, null, Set.of("")) + ); + assertThat(exception.getMessage(), equalTo("OpenSearch Security is not initialized.")); + verify(configModel, never()).getSecurityRoles(); + } + + @Test + public void testEvaluate_NotInitialized_NoSecurityRoles_ExceptionThrown() { + final OpenSearchSecurityException exception = assertThrows( + OpenSearchSecurityException.class, + () -> privilegesEvaluator.evaluate(TEST_USER, null, Set.of("")) + ); + assertThat(exception.getMessage(), equalTo("OpenSearch Security is not initialized.")); + verify(configModel).getSecurityRoles(); + } + + @Test + public void testMapRoles_ReturnsMappedRoles() { + final User user = mock(User.class); + final Set mappedRoles = Collections.singleton("role1"); + when(configModel.mapSecurityRoles(any(), any())).thenReturn(mappedRoles); + + final Set result = privilegesEvaluator.mapRoles(user, null); + + assertThat(result, equalTo(mappedRoles)); + verifyNoInteractions(user); + verify(configModel).mapSecurityRoles(user, null); + } + + @Test + public void testEvaluate_Successful_NewPermission() { + String action = "hw:greet"; + SecurityRoles securityRoles = mock(SecurityRoles.class); + when(configModel.getSecurityRoles()).thenReturn(securityRoles); + when(configModel.getSecurityRoles().filter(Collections.emptySet())).thenReturn(securityRoles); + when(securityRoles.impliesClusterPermissionPermission(action)).thenReturn(true); + + PrivilegesEvaluatorResponse response = privilegesEvaluator.evaluate(TEST_USER, "", Set.of(action)); + + assertThat(response.allowed, equalTo(true)); + verify(securityRoles).impliesClusterPermissionPermission(action); + } + + @Test + public void testEvaluate_Successful_LegacyPermission() { + String action = "cluster:admin/opensearch/hw/greet"; + SecurityRoles securityRoles = mock(SecurityRoles.class); + when(configModel.getSecurityRoles()).thenReturn(securityRoles); + when(configModel.getSecurityRoles().filter(Collections.emptySet())).thenReturn(securityRoles); + when(securityRoles.impliesClusterPermissionPermission(action)).thenReturn(true); + + PrivilegesEvaluatorResponse response = privilegesEvaluator.evaluate(TEST_USER, "", Set.of(action)); + + assertThat(response.allowed, equalTo(true)); + verify(securityRoles).impliesClusterPermissionPermission(action); + verify(configModel, times(3)).getSecurityRoles(); + } + + @Test + public void testEvaluate_Unsuccessful() { + String action = "action"; + SecurityRoles securityRoles = mock(SecurityRoles.class); + when(configModel.getSecurityRoles()).thenReturn(securityRoles); + when(configModel.getSecurityRoles().filter(Collections.emptySet())).thenReturn(securityRoles); + when(securityRoles.impliesClusterPermissionPermission(action)).thenReturn(false); + + PrivilegesEvaluatorResponse response = privilegesEvaluator.evaluate(TEST_USER, "", Set.of(action)); + + assertThat(response.allowed, equalTo(false)); + verify(securityRoles).impliesClusterPermissionPermission(action); + } +} diff --git a/src/test/java/org/opensearch/security/privileges/legacy/SystemIndexAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/legacy/SystemIndexAccessEvaluatorTest.java new file mode 100644 index 0000000000..e9e7d65e9f --- /dev/null +++ b/src/test/java/org/opensearch/security/privileges/legacy/SystemIndexAccessEvaluatorTest.java @@ -0,0 +1,691 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.privileges.legacy; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import org.apache.logging.log4j.Logger; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.get.MultiGetRequest; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.support.IndicesOptions; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; +import org.opensearch.security.securityconf.ConfigModelV7; +import org.opensearch.security.securityconf.SecurityRoles; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; +import org.opensearch.tasks.Task; + +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.opensearch.security.support.ConfigConstants.SYSTEM_INDEX_PERMISSION; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class SystemIndexAccessEvaluatorTest { + + @Mock + private AuditLog auditLog; + @Mock + private IndexResolverReplacer irr; + @Mock + private ActionRequest request; + @Mock + private Task task; + @Mock + private PrivilegesEvaluatorResponse presponse; + @Mock + private Logger log; + @Mock + ClusterService cs; + + private SystemIndexAccessEvaluator evaluator; + private static final String UNPROTECTED_ACTION = "indices:data/read"; + private static final String PROTECTED_ACTION = "indices:data/write"; + + private static final String TEST_SYSTEM_INDEX = ".test_system_index"; + private static final String TEST_INDEX = ".test"; + private static final String SECURITY_INDEX = ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX; + + @Mock + SecurityRoles securityRoles; + + User user; + + IndexNameExpressionResolver indexNameExpressionResolver; + + private ThreadContext createThreadContext() { + return new ThreadContext(Settings.EMPTY); + } + + protected IndexNameExpressionResolver createIndexNameExpressionResolver(ThreadContext threadContext) { + return new IndexNameExpressionResolver(threadContext); + } + + public void setup( + boolean isSystemIndexEnabled, + boolean isSystemIndexPermissionsEnabled, + String index, + boolean createIndexPatternWithSystemIndexPermission + ) { + ThreadContext threadContext = createThreadContext(); + indexNameExpressionResolver = createIndexNameExpressionResolver(threadContext); + + // create a security role + ConfigModelV7.IndexPattern ip = spy(new ConfigModelV7.IndexPattern(index)); + ConfigModelV7.SecurityRole.Builder _securityRole = new ConfigModelV7.SecurityRole.Builder("role_a"); + ip.addPerm(createIndexPatternWithSystemIndexPermission ? Set.of("*", SYSTEM_INDEX_PERMISSION) : Set.of("*")); + _securityRole.addIndexPattern(ip); + _securityRole.addClusterPerms(List.of("*")); + ConfigModelV7.SecurityRole secRole = _securityRole.build(); + + try { + // create an instance of Security Role + Constructor constructor = ConfigModelV7.SecurityRoles.class.getDeclaredConstructor(int.class); + constructor.setAccessible(true); + securityRoles = constructor.newInstance(1); + + // add security role to Security Roles + Method addSecurityRoleMethod = ConfigModelV7.SecurityRoles.class.getDeclaredMethod( + "addSecurityRole", + ConfigModelV7.SecurityRole.class + ); + addSecurityRoleMethod.setAccessible(true); + addSecurityRoleMethod.invoke(securityRoles, secRole); + + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new RuntimeException(e); + } + + // create a user and associate them with the role + user = new User("user_a"); + user.addSecurityRoles(List.of("role_a")); + + // when trying to resolve Index Names + + evaluator = new SystemIndexAccessEvaluator( + Settings.builder() + .put(ConfigConstants.SECURITY_SYSTEM_INDICES_KEY, TEST_SYSTEM_INDEX) + .put(ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY, isSystemIndexEnabled) + .put(ConfigConstants.SECURITY_SYSTEM_INDICES_PERMISSIONS_ENABLED_KEY, isSystemIndexPermissionsEnabled) + .build(), + auditLog, + irr + ); + evaluator.log = log; + + when(log.isDebugEnabled()).thenReturn(true); + when(log.isInfoEnabled()).thenReturn(true); + + doReturn(ImmutableSet.of(index)).when(ip).getResolvedIndexPattern(user, indexNameExpressionResolver, cs, true, false); + } + + @After + public void after() { + verifyNoMoreInteractions(auditLog, irr, request, task, presponse, log); + } + + @Test + public void testUnprotectedActionOnRegularIndex_systemIndexDisabled() { + setup(false, false, TEST_INDEX, false); + final Resolved resolved = createResolved(TEST_INDEX); + + // Action + final PrivilegesEvaluatorResponse response = evaluator.evaluate( + request, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + verifyNoInteractions(presponse); + assertThat(response, is(presponse)); + + } + + @Test + public void testUnprotectedActionOnRegularIndex_systemIndexPermissionDisabled() { + setup(true, false, TEST_INDEX, false); + final Resolved resolved = createResolved(TEST_INDEX); + + // Action + final PrivilegesEvaluatorResponse response = evaluator.evaluate( + request, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + verifyNoInteractions(presponse); + assertThat(response, is(presponse)); + } + + @Test + public void testUnprotectedActionOnRegularIndex_systemIndexPermissionEnabled() { + setup(true, true, TEST_INDEX, false); + final Resolved resolved = createResolved(TEST_INDEX); + + // Action + final PrivilegesEvaluatorResponse response = evaluator.evaluate( + request, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + verifyNoInteractions(presponse); + assertThat(response, is(presponse)); + } + + @Test + public void testUnprotectedActionOnSystemIndex_systemIndexDisabled() { + setup(false, false, TEST_SYSTEM_INDEX, false); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + final PrivilegesEvaluatorResponse response = evaluator.evaluate( + request, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + verifyNoInteractions(presponse); + assertThat(response, is(presponse)); + } + + @Test + public void testUnprotectedActionOnSystemIndex_systemIndexPermissionDisabled() { + setup(true, false, TEST_SYSTEM_INDEX, false); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + final PrivilegesEvaluatorResponse response = evaluator.evaluate( + request, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + verifyNoInteractions(presponse); + assertThat(response, is(presponse)); + } + + @Test + public void testUnprotectedActionOnSystemIndex_systemIndexPermissionEnabled_WithoutSystemIndexPermission() { + setup(true, true, TEST_SYSTEM_INDEX, false); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + final PrivilegesEvaluatorResponse response = evaluator.evaluate( + request, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + verify(presponse).markComplete(); + assertThat(response, is(presponse)); + + verify(auditLog).logSecurityIndexAttempt(request, UNPROTECTED_ACTION, null); + verify(log).isInfoEnabled(); + verify(log).info("No {} permission for user roles {} to System Indices {}", UNPROTECTED_ACTION, securityRoles, TEST_SYSTEM_INDEX); + } + + @Test + public void testUnprotectedActionOnSystemIndex_systemIndexPermissionEnabled_WithSystemIndexPermission() { + setup(true, true, TEST_SYSTEM_INDEX, true); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + final PrivilegesEvaluatorResponse response = evaluator.evaluate( + request, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + assertThat(response, is(presponse)); + // unprotected action is not allowed on a system index + assertThat(presponse.allowed, is(false)); + } + + @Test + public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexDisabled() { + setup(false, false, TEST_SYSTEM_INDEX, false); + + final SearchRequest searchRequest = mock(SearchRequest.class); + final MultiGetRequest realtimeRequest = mock(MultiGetRequest.class); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate( + searchRequest, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + evaluator.evaluate( + realtimeRequest, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + + verifyNoInteractions(presponse); + } + + @Test + public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionDisabled() { + setup(true, false, TEST_SYSTEM_INDEX, false); + + final SearchRequest searchRequest = mock(SearchRequest.class); + final MultiGetRequest realtimeRequest = mock(MultiGetRequest.class); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate( + searchRequest, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + evaluator.evaluate( + realtimeRequest, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + + verify(searchRequest).requestCache(Boolean.FALSE); + verify(realtimeRequest).realtime(Boolean.FALSE); + + verify(log, times(2)).isDebugEnabled(); + verify(log).debug("Disable search request cache for this request"); + verify(log).debug("Disable realtime for this request"); + } + + @Test + public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled_withoutSystemIndexPermission() { + setup(true, true, TEST_SYSTEM_INDEX, false); + + final SearchRequest searchRequest = mock(SearchRequest.class); + final MultiGetRequest realtimeRequest = mock(MultiGetRequest.class); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate( + searchRequest, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + evaluator.evaluate( + realtimeRequest, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + + verify(searchRequest).requestCache(Boolean.FALSE); + verify(realtimeRequest).realtime(Boolean.FALSE); + + verify(log, times(2)).isDebugEnabled(); + verify(log).debug("Disable search request cache for this request"); + verify(log).debug("Disable realtime for this request"); + verify(auditLog).logSecurityIndexAttempt(request, UNPROTECTED_ACTION, null); + verify(auditLog).logSecurityIndexAttempt(searchRequest, UNPROTECTED_ACTION, null); + verify(auditLog).logSecurityIndexAttempt(realtimeRequest, UNPROTECTED_ACTION, null); + verify(presponse, times(3)).markComplete(); + verify(log, times(2)).isDebugEnabled(); + verify(log, times(3)).isInfoEnabled(); + verify(log, times(3)).info( + "No {} permission for user roles {} to System Indices {}", + UNPROTECTED_ACTION, + securityRoles, + TEST_SYSTEM_INDEX + ); + verify(log).debug("Disable search request cache for this request"); + verify(log).debug("Disable realtime for this request"); + } + + @Test + public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled_withSystemIndexPermission() { + setup(true, true, TEST_SYSTEM_INDEX, true); + + final SearchRequest searchRequest = mock(SearchRequest.class); + final MultiGetRequest realtimeRequest = mock(MultiGetRequest.class); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate( + searchRequest, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + evaluator.evaluate( + realtimeRequest, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + + verify(searchRequest).requestCache(Boolean.FALSE); + verify(realtimeRequest).realtime(Boolean.FALSE); + + verify(log, times(2)).isDebugEnabled(); + verify(log).debug("Disable search request cache for this request"); + verify(log).debug("Disable realtime for this request"); + } + + @Test + public void testProtectedActionLocalAll_systemIndexDisabled() { + setup(false, false, TEST_SYSTEM_INDEX, false); + final Resolved resolved = Resolved._LOCAL_ALL; + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); + assertThat(presponse.allowed, is(false)); + verify(presponse).markComplete(); + verify(log).warn("{} for '_all' indices is not allowed for a regular user", "indices:data/write"); + } + + @Test + public void testProtectedActionLocalAll_systemIndexPermissionDisabled() { + setup(true, false, TEST_SYSTEM_INDEX, false); + final Resolved resolved = Resolved._LOCAL_ALL; + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); + assertThat(presponse.allowed, is(false)); + verify(presponse).markComplete(); + verify(log).warn("{} for '_all' indices is not allowed for a regular user", PROTECTED_ACTION); + } + + @Test + public void testProtectedActionLocalAll_systemIndexPermissionEnabled() { + setup(true, true, TEST_SYSTEM_INDEX, false); + final Resolved resolved = Resolved._LOCAL_ALL; + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); + assertThat(presponse.allowed, is(false)); + verify(presponse).markComplete(); + verify(log).warn("{} for '_all' indices is not allowed for a regular user", PROTECTED_ACTION); + } + + @Test + public void testProtectedActionOnRegularIndex_systemIndexDisabled() { + setup(false, false, TEST_INDEX, false); + final Resolved resolved = createResolved(TEST_INDEX); + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + assertThat(presponse.allowed, is(false)); + } + + @Test + public void testProtectedActionOnRegularIndex_systemIndexPermissionDisabled() { + setup(true, false, TEST_INDEX, false); + final Resolved resolved = createResolved(TEST_INDEX); + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + assertThat(presponse.allowed, is(false)); + } + + @Test + public void testProtectedActionOnRegularIndex_systemIndexPermissionEnabled() { + setup(true, true, TEST_INDEX, false); + final Resolved resolved = createResolved(TEST_INDEX); + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + assertThat(presponse.allowed, is(false)); + } + + @Test + public void testProtectedActionOnSystemIndex_systemIndexDisabled() { + setup(false, false, TEST_SYSTEM_INDEX, false); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + assertThat(presponse.allowed, is(false)); + } + + @Test + public void testProtectedActionOnSystemIndex_systemIndexPermissionDisabled() { + setup(true, false, TEST_SYSTEM_INDEX, false); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); + assertThat(presponse.allowed, is(false)); + verify(presponse).markComplete(); + verify(log).warn("{} for '{}' index is not allowed for a regular user", PROTECTED_ACTION, TEST_SYSTEM_INDEX); + } + + @Test + public void testProtectedActionOnSystemIndex_systemIndexPermissionEnabled_withoutSystemIndexPermission() { + setup(true, true, TEST_SYSTEM_INDEX, false); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); + assertThat(presponse.allowed, is(false)); + verify(presponse).markComplete(); + verify(log).isInfoEnabled(); + verify(log).info("No {} permission for user roles {} to System Indices {}", PROTECTED_ACTION, securityRoles, TEST_SYSTEM_INDEX); + } + + @Test + public void testProtectedActionOnSystemIndex_systemIndexPermissionEnabled_withSystemIndexPermission() { + + setup(true, true, TEST_SYSTEM_INDEX, true); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + assertThat(presponse.allowed, is(false)); + } + + @Test + public void testProtectedActionOnProtectedSystemIndex_systemIndexDisabled() { + setup(false, false, SECURITY_INDEX, false); + final Resolved resolved = createResolved(SECURITY_INDEX); + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); + assertThat(presponse.allowed, is(false)); + verify(presponse).markComplete(); + + verify(log).warn("{} for '{}' index is not allowed for a regular user", PROTECTED_ACTION, SECURITY_INDEX); + } + + @Test + public void testProtectedActionOnProtectedSystemIndex_systemIndexPermissionDisabled() { + setup(true, false, SECURITY_INDEX, false); + final Resolved resolved = createResolved(SECURITY_INDEX); + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); + assertThat(presponse.allowed, is(false)); + verify(presponse).markComplete(); + + verify(log).warn("{} for '{}' index is not allowed for a regular user", PROTECTED_ACTION, SECURITY_INDEX); + } + + @Test + public void testUnprotectedActionOnProtectedSystemIndex_systemIndexPermissionEnabled_withoutSystemIndexPermission() { + testSecurityIndexAccess(UNPROTECTED_ACTION); + } + + @Test + public void testUnprotectedActionOnProtectedSystemIndex_systemIndexPermissionEnabled_withSystemIndexPermission() { + testSecurityIndexAccess(UNPROTECTED_ACTION); + } + + @Test + public void testProtectedActionOnProtectedSystemIndex_systemIndexPermissionEnabled_withoutSystemIndexPermission() { + testSecurityIndexAccess(PROTECTED_ACTION); + } + + @Test + public void testProtectedActionOnProtectedSystemIndex_systemIndexPermissionEnabled_withSystemIndexPermission() { + testSecurityIndexAccess(PROTECTED_ACTION); + } + + private void testSecurityIndexAccess(String action) { + setup(true, true, SECURITY_INDEX, true); + + final Resolved resolved = createResolved(SECURITY_INDEX); + + // Action + evaluator.evaluate(request, task, action, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + verify(auditLog).logSecurityIndexAttempt(request, action, task); + assertThat(presponse.allowed, is(false)); + verify(presponse).markComplete(); + + verify(log).isInfoEnabled(); + verify(log).info("{} not permitted for a regular user {} on protected system indices {}", action, securityRoles, SECURITY_INDEX); + } + + private Resolved createResolved(final String... indexes) { + return new Resolved( + ImmutableSet.of(), + ImmutableSet.copyOf(indexes), + ImmutableSet.copyOf(indexes), + ImmutableSet.of(), + IndicesOptions.STRICT_EXPAND_OPEN + ); + } +} diff --git a/src/test/java/org/opensearch/security/protected_indices/ProtectedIndicesTests.java b/src/test/java/org/opensearch/security/protected_indices/ProtectedIndicesTests.java index 03f11d74c0..057c49b99e 100644 --- a/src/test/java/org/opensearch/security/protected_indices/ProtectedIndicesTests.java +++ b/src/test/java/org/opensearch/security/protected_indices/ProtectedIndicesTests.java @@ -28,8 +28,11 @@ package org.opensearch.security.protected_indices; import java.util.Arrays; +import java.util.Collection; import java.util.List; +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import org.apache.http.Header; import org.apache.http.HttpStatus; import org.junit.Test; @@ -49,6 +52,7 @@ import org.opensearch.core.rest.RestStatus; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.test.DynamicSecurityConfig; import org.opensearch.security.test.SingleClusterTest; @@ -60,6 +64,17 @@ public class ProtectedIndicesTests extends SingleClusterTest { + private boolean useOldPrivilegeEvaluationImplementation; + + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + + public ProtectedIndicesTests(@Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation) { + this.useOldPrivilegeEvaluationImplementation = useOldPrivilegeEvaluationImplementation; + } + private static final List listOfIndexesToTest = Arrays.asList("logs1", "logs2", "logs3", "no_match"); private static final List listOfIndexPatternsToTest = Arrays.asList("*logs*", "logs*", "*lo*"); private static final List protectedIndexRoles = Arrays.asList("protected_index_role1", "protected_index_role2"); @@ -88,6 +103,7 @@ public void setupSettingsEnabled() throws Exception { .put(ConfigConstants.SECURITY_PROTECTED_INDICES_ENABLED_KEY, true) .putList(ConfigConstants.SECURITY_PROTECTED_INDICES_KEY, listOfIndexesToTest) .putList(ConfigConstants.SECURITY_PROTECTED_INDICES_ROLES_KEY, protectedIndexRoles) + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) .build(); setup( Settings.EMPTY, @@ -106,6 +122,7 @@ public void setupSettingsIndexPatterns() throws Exception { .put(ConfigConstants.SECURITY_PROTECTED_INDICES_ENABLED_KEY, true) .putList(ConfigConstants.SECURITY_PROTECTED_INDICES_KEY, listOfIndexPatternsToTest) .putList(ConfigConstants.SECURITY_PROTECTED_INDICES_ROLES_KEY, protectedIndexRoles) + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) .build(); setup( Settings.EMPTY, @@ -131,6 +148,7 @@ public void setupSettingsDisabled() throws Exception { .put(ConfigConstants.SECURITY_PROTECTED_INDICES_ENABLED_KEY, false) .putList(ConfigConstants.SECURITY_PROTECTED_INDICES_KEY, listOfIndexesToTest) .putList(ConfigConstants.SECURITY_PROTECTED_INDICES_ROLES_KEY, protectedIndexRoles) + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) .build(); setup( Settings.EMPTY, @@ -149,6 +167,7 @@ public void setupSettingsEnabledSnapshot() throws Exception { .put(ConfigConstants.SECURITY_PROTECTED_INDICES_ENABLED_KEY, true) .putList(ConfigConstants.SECURITY_PROTECTED_INDICES_KEY, listOfIndexesToTest) .putList(ConfigConstants.SECURITY_PROTECTED_INDICES_ROLES_KEY, protectedIndexRoles) + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) .build(); setup( diff --git a/src/test/java/org/opensearch/security/system_indices/AbstractSystemIndicesTests.java b/src/test/java/org/opensearch/security/system_indices/AbstractSystemIndicesTests.java index 2e154060e9..ed250db6c8 100644 --- a/src/test/java/org/opensearch/security/system_indices/AbstractSystemIndicesTests.java +++ b/src/test/java/org/opensearch/security/system_indices/AbstractSystemIndicesTests.java @@ -14,6 +14,7 @@ import java.io.IOException; import java.util.List; +import com.carrotsearch.randomizedtesting.annotations.Name; import org.apache.http.Header; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; @@ -31,6 +32,7 @@ import org.opensearch.core.rest.RestStatus; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.test.DynamicSecurityConfig; import org.opensearch.security.test.SingleClusterTest; @@ -80,6 +82,12 @@ public abstract class AbstractSystemIndicesTests extends SingleClusterTest { static final String updateIndexSettings = "{\n" + " \"index\" : {\n" + " \"refresh_interval\" : null\n" + " }\n" + "}"; static final String newMappings = "{\"properties\": {" + "\"user_name\": {" + "\"type\": \"text\"" + "}}}"; + protected final boolean useOldPrivilegeEvaluationImplementation; + + public AbstractSystemIndicesTests(@Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation) { + this.useOldPrivilegeEvaluationImplementation = useOldPrivilegeEvaluationImplementation; + } + void setupWithSsl(boolean isSystemIndexEnabled, boolean isSystemIndexPermissionEnabled) throws Exception { Settings systemIndexSettings = Settings.builder() @@ -90,6 +98,7 @@ void setupWithSsl(boolean isSystemIndexEnabled, boolean isSystemIndexPermissionE .put("plugins.security.ssl.http.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("node-0-keystore.jks")) .put("plugins.security.ssl.http.truststore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("truststore.jks")) .put("path.repo", repositoryPath.getRoot().getAbsolutePath()) + .put(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), useOldPrivilegeEvaluationImplementation) .build(); setup( Settings.EMPTY, diff --git a/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java b/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java index eb56cc6fb6..7c0a09c715 100644 --- a/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java +++ b/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java @@ -12,7 +12,11 @@ package org.opensearch.security.system_indices; import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import org.apache.http.Header; import org.apache.http.HttpStatus; import org.junit.Before; @@ -33,6 +37,15 @@ */ public class SystemIndexDisabledTests extends AbstractSystemIndicesTests { + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + + public SystemIndexDisabledTests(@Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation) { + super(useOldPrivilegeEvaluationImplementation); + } + @Before public void setup() throws Exception { setupWithSsl(false, false); diff --git a/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionDisabledTests.java b/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionDisabledTests.java index da868aa5f8..a2daa9fae0 100644 --- a/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionDisabledTests.java +++ b/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionDisabledTests.java @@ -12,7 +12,11 @@ package org.opensearch.security.system_indices; import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import org.apache.http.Header; import org.apache.http.HttpStatus; import org.junit.Before; @@ -31,6 +35,16 @@ * Adds test for scenario when system index feature is enabled, but system index permission feature is disabled */ public class SystemIndexPermissionDisabledTests extends AbstractSystemIndicesTests { + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + + public SystemIndexPermissionDisabledTests( + @Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation + ) { + super(useOldPrivilegeEvaluationImplementation); + } @Before public void setup() throws Exception { diff --git a/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java b/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java index 4c22b2ed50..8a694d2410 100644 --- a/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java +++ b/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java @@ -11,6 +11,11 @@ package org.opensearch.security.system_indices; +import java.util.Arrays; +import java.util.Collection; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import org.apache.http.Header; import org.apache.http.HttpStatus; import org.junit.Before; @@ -26,6 +31,16 @@ import static org.junit.Assert.assertFalse; public class SystemIndexPermissionEnabledTests extends AbstractSystemIndicesTests { + @ParametersFactory() + public static Collection params() { + return Arrays.asList(new Object[] { false }, new Object[] { true }); + } + + public SystemIndexPermissionEnabledTests( + @Name("useOldPrivilegeEvaluationImplementation") boolean useOldPrivilegeEvaluationImplementation + ) { + super(useOldPrivilegeEvaluationImplementation); + } @Before public void setup() throws Exception { diff --git a/src/test/java/org/opensearch/security/util/MockIndexMetadataBuilder.java b/src/test/java/org/opensearch/security/util/MockIndexMetadataBuilder.java new file mode 100644 index 0000000000..e8af0f1384 --- /dev/null +++ b/src/test/java/org/opensearch/security/util/MockIndexMetadataBuilder.java @@ -0,0 +1,194 @@ +/* + * 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. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.security.util; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableMap; + +import org.opensearch.Version; +import org.opensearch.cluster.metadata.AliasMetadata; +import org.opensearch.cluster.metadata.DataStream; +import org.opensearch.cluster.metadata.IndexAbstraction; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.index.Index; + +/** + * Creates mocks of org.opensearch.cluster.metadata.IndexAbstraction maps. Useful for unit testing code which + * operates on index metadata. + * + * TODO: This is the evil twin of the same class in the integrationTest module. Possibly tests depending on this + * should be moved to the integrationTest module? + */ +public class MockIndexMetadataBuilder { + + private final static Settings INDEX_SETTINGS = Settings.builder() + .put(IndexMetadata.SETTING_INDEX_VERSION_CREATED.getKey(), Version.CURRENT) + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 1) + .build(); + + private Map nameToIndexAbstractionMap = new HashMap<>(); + private Map nameToIndexMetadataMap = new HashMap<>(); + private Map> indicesToAliases = new HashMap<>(); + private Map> aliasesToIndices = new HashMap<>(); + + public static MockIndexMetadataBuilder indices(String... indices) { + MockIndexMetadataBuilder builder = new MockIndexMetadataBuilder(); + + for (String index : indices) { + builder.index(index); + } + + return builder; + } + + public static MockIndexMetadataBuilder dataStreams(String... dataStreams) { + MockIndexMetadataBuilder builder = new MockIndexMetadataBuilder(); + + for (String dataStream : dataStreams) { + builder.dataStream(dataStream); + } + + return builder; + } + + public ImmutableMap build() { + Map aliasMetadataMap = new HashMap<>(); + + for (Map.Entry> aliasEntry : this.aliasesToIndices.entrySet()) { + String alias = aliasEntry.getKey(); + AliasMetadata aliasMetadata = AliasMetadata.builder(alias).build(); + aliasMetadataMap.put(alias, aliasMetadata); + } + + for (Map.Entry> indexEntry : this.indicesToAliases.entrySet()) { + String index = indexEntry.getKey(); + Set aliases = indexEntry.getValue(); + + IndexMetadata.Builder indexMetadataBuilder = IndexMetadata.builder(index).settings(INDEX_SETTINGS); + + for (String alias : aliases) { + indexMetadataBuilder.putAlias(aliasMetadataMap.get(alias)); + } + + IndexMetadata indexMetadata = indexMetadataBuilder.build(); + nameToIndexMetadataMap.put(index, indexMetadata); + nameToIndexAbstractionMap.put(index, new IndexAbstraction.Index(indexMetadata)); + } + + for (Map.Entry> aliasEntry : this.aliasesToIndices.entrySet()) { + String alias = aliasEntry.getKey(); + Set indices = aliasEntry.getValue(); + AliasMetadata aliasMetadata = aliasMetadataMap.get(alias); + + String firstIndex = indices.iterator().next(); + indices.remove(firstIndex); + + IndexMetadata firstIndexMetadata = nameToIndexMetadataMap.get(firstIndex); + IndexAbstraction.Alias indexAbstraction = new IndexAbstraction.Alias(aliasMetadata, firstIndexMetadata); + + for (String index : indices) { + indexAbstraction.getIndices().add(nameToIndexMetadataMap.get(index)); + } + + nameToIndexAbstractionMap.put(alias, indexAbstraction); + } + + return ImmutableMap.copyOf(this.nameToIndexAbstractionMap); + } + + public MockIndexMetadataBuilder index(String index) { + if (!this.indicesToAliases.containsKey(index)) { + this.indicesToAliases.put(index, new HashSet<>()); + } + return this; + } + + public AliasBuilder alias(String alias) { + return new AliasBuilder(alias); + } + + public MockIndexMetadataBuilder dataStream(String dataStream) { + return dataStream(dataStream, 3); + } + + public MockIndexMetadataBuilder dataStream(String dataStream, int generations) { + List backingIndices = new ArrayList<>(); + + for (int i = 1; i <= generations; i++) { + String backingIndexName = DataStream.getDefaultBackingIndexName(dataStream, i); + backingIndices.add(new Index(backingIndexName, backingIndexName)); + } + + DataStream dataStreamMetadata = new DataStream(dataStream, new DataStream.TimestampField("@timestamp"), backingIndices); + IndexAbstraction.DataStream dataStreamIndexAbstraction = new IndexAbstraction.DataStream( + dataStreamMetadata, + backingIndices.stream().map(i -> getIndexMetadata(i.getName())).collect(Collectors.toList()) + ); + this.nameToIndexAbstractionMap.put(dataStream, dataStreamIndexAbstraction); + + for (Index backingIndex : backingIndices) { + this.nameToIndexAbstractionMap.put( + backingIndex.getName(), + new IndexAbstraction.Index(getIndexMetadata(backingIndex.getName()), dataStreamIndexAbstraction) + ); + } + + return this; + } + + private IndexMetadata getIndexMetadata(String index) { + IndexMetadata result = this.nameToIndexMetadataMap.get(index); + + if (result == null) { + result = IndexMetadata.builder(index) + .settings(Settings.builder().put(IndexMetadata.SETTING_INDEX_VERSION_CREATED.getKey(), Version.CURRENT)) + .numberOfShards(1) + .numberOfReplicas(1) + .build(); + this.nameToIndexMetadataMap.put(index, result); + } + + return result; + } + + public class AliasBuilder { + private String alias; + + private AliasBuilder(String alias) { + this.alias = alias; + } + + public MockIndexMetadataBuilder of(String firstIndex, String... moreIndices) { + MockIndexMetadataBuilder.this.indicesToAliases.computeIfAbsent(firstIndex, (k) -> new HashSet<>()).add(this.alias); + + Set indices = new HashSet<>(); + indices.add(firstIndex); + + for (String index : moreIndices) { + MockIndexMetadataBuilder.this.indicesToAliases.computeIfAbsent(index, (k) -> new HashSet<>()).add(this.alias); + indices.add(index); + } + + MockIndexMetadataBuilder.this.aliasesToIndices.put(this.alias, indices); + + return MockIndexMetadataBuilder.this; + } + } +}