From 9caf5cbaa90c2ccbaf8b0bf00ee8bb1fe9326919 Mon Sep 17 00:00:00 2001 From: Prabhas Kurapati <66924475+prabhask5@users.noreply.github.com> Date: Mon, 24 Jun 2024 08:57:44 -0700 Subject: [PATCH] [Enhancement-3139] Make FLS, DLS and Field Masking Unit Tests more extensive by testing more document retrieval APIs (#4417) Signed-off-by: Prabhas Kurapati Signed-off-by: Prabhas Kurapati <66924475+prabhask5@users.noreply.github.com> --- .../security/DlsIntegrationTests.java | 154 +++ .../security/FlsAndFieldMaskingTests.java | 888 +++++++++++++++++- 2 files changed, 1041 insertions(+), 1 deletion(-) diff --git a/src/integrationTest/java/org/opensearch/security/DlsIntegrationTests.java b/src/integrationTest/java/org/opensearch/security/DlsIntegrationTests.java index 3e3ac61502..31fc2b7c7e 100644 --- a/src/integrationTest/java/org/opensearch/security/DlsIntegrationTests.java +++ b/src/integrationTest/java/org/opensearch/security/DlsIntegrationTests.java @@ -11,6 +11,7 @@ import java.io.IOException; import java.io.Serializable; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -25,6 +26,11 @@ import org.junit.runner.RunWith; import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; +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.MultiGetResponse; import org.opensearch.action.search.SearchRequest; import org.opensearch.action.search.SearchResponse; import org.opensearch.client.Client; @@ -37,6 +43,8 @@ import org.opensearch.test.framework.cluster.LocalCluster; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @@ -57,6 +65,7 @@ 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.searchRequestWithSort; +import static org.opensearch.test.framework.matcher.GetResponseMatchers.containDocument; import static org.opensearch.test.framework.matcher.SearchResponseMatchers.containAggregationWithNameAndType; import static org.opensearch.test.framework.matcher.SearchResponseMatchers.isSuccessfulSearchResponse; import static org.opensearch.test.framework.matcher.SearchResponseMatchers.numberOfTotalHitsIsEqualTo; @@ -164,6 +173,36 @@ public class DlsIntegrationTests { .on("*") ); + static final TestSecurityConfig.Role ROLE_MATCH_ARTIST_BOOL_QUERY = new TestSecurityConfig.Role("test_role_bool").clusterPermissions( + "cluster_composite_ops_ro" + ).indexPermissions("read").dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_ARTIST, ARTIST_FIRST)).on(FIRST_INDEX_NAME); + + static final TestSecurityConfig.Role ROLE_MATCH_STARS_TERM_QUERY = new TestSecurityConfig.Role("test_role_term").clusterPermissions( + "cluster_composite_ops_ro" + ).indexPermissions("read").dls(String.format("{\"term\":{\"%s\":%d}}", FIELD_STARS, 1)).on(FIRST_INDEX_NAME); + + /** + * User with a role with DLS restrictions containing a bool query in which the artist field needs to match ARTIST_FIRST. + */ + static final TestSecurityConfig.User USER_MATCH_ARTIST_BOOL_QUERY = new TestSecurityConfig.User("bool_user").roles( + ROLE_MATCH_ARTIST_BOOL_QUERY + ); + + /** + * User with a role with DLS restrictions containing a term query in which the stars field needs to match 1. + */ + static final TestSecurityConfig.User USER_MATCH_STARS_TERM_QUERY = new TestSecurityConfig.User("term_user").roles( + ROLE_MATCH_STARS_TERM_QUERY + ); + + /** + * User with two roles: a role with DLS restrictions containing a bool query in which the artist field needs to match ARTIST_FIRST, and a role with DLS restrictions containing a term query in which the stars field needs to match 1. + */ + + static final TestSecurityConfig.User USER_BOTH_MATCH_ARTIST_BOOL_QUERY_MATCH_STARS_TERM_QUERY = new TestSecurityConfig.User( + "bool_term_user" + ).roles(ROLE_MATCH_ARTIST_BOOL_QUERY, ROLE_MATCH_STARS_TERM_QUERY); + /** * Test role 1 for DLS filtering with two (non)overlapping roles. This role imposes a filter where the user can only access documents where the sensitive field is false. This role is applied at a higher level for all index patterns. */ @@ -235,6 +274,9 @@ public class DlsIntegrationTests { READ_WHERE_STARS_LESS_THAN_THREE, READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_FIELD_STARS_GREATER_THAN_FIVE, READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_MATCHES_ARTIST_FIRST, + USER_MATCH_ARTIST_BOOL_QUERY, + USER_MATCH_STARS_TERM_QUERY, + USER_BOTH_MATCH_ARTIST_BOOL_QUERY_MATCH_STARS_TERM_QUERY, USER_NON_SENSITIVE_ONLY, USER_ALLOW_ALL, USER_MATCH_HISTORY_GENRE_ONLY, @@ -605,6 +647,118 @@ public void testAggregateAndComputeStarRatings() throws IOException { } @Test + public void testGetDocumentWithBoolOrTermDLSRestrictions() throws IOException, Exception { + GetRequest findExistingDoc = new GetRequest(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1); + GetRequest findNonExistingDoc = new GetRequest(FIRST_INDEX_NAME, "RANDOM_INDEX"); + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_MATCH_ARTIST_BOOL_QUERY)) { + assertGetForDLSRestrictions(restHighLevelClient, findExistingDoc, findNonExistingDoc); + } + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_MATCH_STARS_TERM_QUERY)) { + assertGetForDLSRestrictions(restHighLevelClient, findExistingDoc, findNonExistingDoc); + } + } + + private void assertGetForDLSRestrictions( + RestHighLevelClient restHighLevelClient, + GetRequest findExistingDoc, + GetRequest findNonExistingDoc + ) throws IOException, Exception { + GetResponse response = restHighLevelClient.get(findExistingDoc, DEFAULT); + assertThat(response, containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1)); + response = restHighLevelClient.get(findNonExistingDoc, DEFAULT); + assertThat(response.isExists(), equalTo(false)); + } + + @Test + public void testMultiGetDocumentWithBoolOrTermDLSRestrictions() throws IOException, Exception { + MultiGetRequest multiGetRequest = new MultiGetRequest(); + multiGetRequest.add(new MultiGetRequest.Item(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1)); + multiGetRequest.add(new MultiGetRequest.Item(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_2)); + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_MATCH_ARTIST_BOOL_QUERY)) { + assertMGetForDLSRestrictions(restHighLevelClient, multiGetRequest); + } + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_MATCH_STARS_TERM_QUERY)) { + assertMGetForDLSRestrictions(restHighLevelClient, multiGetRequest); + } + } + + private void assertMGetForDLSRestrictions(RestHighLevelClient restHighLevelClient, MultiGetRequest multiGetRequest) throws IOException, + Exception { + MultiGetResponse multiGetResponse = restHighLevelClient.mget(multiGetRequest, DEFAULT); + List getResponses = Arrays.stream(multiGetResponse.getResponses()) + .map(MultiGetItemResponse::getResponse) + .collect(Collectors.toList()); + assertThat(getResponses, hasItem(containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1))); + assertThat(getResponses, not(hasItem(containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_2)))); + } + + @Test + public void testSearchDocumentWithBoolOrTermDLSRestrictions() throws IOException, Exception { + SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_MATCH_ARTIST_BOOL_QUERY)) { + assertSearchForDLSRestrictions(restHighLevelClient, searchRequest); + } + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_MATCH_STARS_TERM_QUERY)) { + assertSearchForDLSRestrictions(restHighLevelClient, searchRequest); + } + } + + @SuppressWarnings("unchecked") + private void assertSearchForDLSRestrictions(RestHighLevelClient restHighLevelClient, SearchRequest searchRequest) throws IOException, + Exception { + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentsInAnyOrder(Pair.of(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1))); + } + + @Test + public void testGetDocumentWithBoolAndTermDLSRestrictions() throws IOException, Exception { + GetRequest findExistingDoc = new GetRequest(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1); + GetRequest findNonExistingDoc = new GetRequest(FIRST_INDEX_NAME, "RANDOM_INDEX"); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_BOTH_MATCH_ARTIST_BOOL_QUERY_MATCH_STARS_TERM_QUERY + ) + ) { + assertGetForDLSRestrictions(restHighLevelClient, findExistingDoc, findNonExistingDoc); + } + } + + @Test + public void testMultiGetDocumentWithBoolAndTermDLSRestrictions() throws IOException, Exception { + MultiGetRequest multiGetRequest = new MultiGetRequest(); + multiGetRequest.add(new MultiGetRequest.Item(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1)); + multiGetRequest.add(new MultiGetRequest.Item(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_2)); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_BOTH_MATCH_ARTIST_BOOL_QUERY_MATCH_STARS_TERM_QUERY + ) + ) { + assertMGetForDLSRestrictions(restHighLevelClient, multiGetRequest); + } + } + + @Test + public void testSearchDocumentWithBoolAndTermDLSRestrictions() throws IOException, Exception { + SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_BOTH_MATCH_ARTIST_BOOL_QUERY_MATCH_STARS_TERM_QUERY + ) + ) { + assertSearchForDLSRestrictions(restHighLevelClient, searchRequest); + } + } + public void testOverlappingRoleUnionSearchFiltering() throws Exception { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_NON_SENSITIVE_ONLY)) { SearchRequest searchRequest = new SearchRequest(UNION_TEST_INDEX_NAME); diff --git a/src/integrationTest/java/org/opensearch/security/FlsAndFieldMaskingTests.java b/src/integrationTest/java/org/opensearch/security/FlsAndFieldMaskingTests.java index 1d057f3c3b..a2409c22bd 100644 --- a/src/integrationTest/java/org/opensearch/security/FlsAndFieldMaskingTests.java +++ b/src/integrationTest/java/org/opensearch/security/FlsAndFieldMaskingTests.java @@ -21,6 +21,7 @@ import java.util.stream.Stream; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.hamcrest.Matcher; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; @@ -64,8 +65,10 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.everyItem; import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +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; @@ -74,6 +77,7 @@ import static org.opensearch.security.Song.ARTIST_STRING; import static org.opensearch.security.Song.ARTIST_TWINS; import static org.opensearch.security.Song.FIELD_ARTIST; +import static org.opensearch.security.Song.FIELD_GENRE; import static org.opensearch.security.Song.FIELD_LYRICS; import static org.opensearch.security.Song.FIELD_STARS; import static org.opensearch.security.Song.FIELD_TITLE; @@ -214,6 +218,68 @@ public class FlsAndFieldMaskingTests { .on("*") ); + static final TestSecurityConfig.Role ROLE_ONLY_FIELD_TITLE_FLS = new TestSecurityConfig.Role("example_inclusive_fls") + .clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .fls(FIELD_TITLE) + .on(FIRST_INDEX_NAME); + + static final TestSecurityConfig.Role ROLE_NO_FIELD_TITLE_FLS = new TestSecurityConfig.Role("example_exclusive_fls").clusterPermissions( + "cluster_composite_ops_ro" + ).indexPermissions("read").fls(String.format("~%s", FIELD_TITLE)).on(FIRST_INDEX_NAME); + + static final TestSecurityConfig.Role ROLE_ONLY_FIELD_TITLE_MASKED = new TestSecurityConfig.Role("example_mask").clusterPermissions( + "cluster_composite_ops_ro" + ).indexPermissions("read").maskedFields(FIELD_TITLE.concat("::/(?<=.{1})./::").concat(MASK_VALUE)).on(FIRST_INDEX_NAME); + + /** + * Example user with fls filter in which the user can only see the {@link Song#FIELD_TITLE} field. + */ + static final TestSecurityConfig.User USER_ONLY_FIELD_TITLE_FLS = new TestSecurityConfig.User("inclusive_fls_user").roles( + ROLE_ONLY_FIELD_TITLE_FLS + ); + + /** + * Example user with fls filter in which the user can see every field but the {@link Song#FIELD_TITLE} field. + */ + static final TestSecurityConfig.User USER_NO_FIELD_TITLE_FLS = new TestSecurityConfig.User("exclusive_fls_user").roles( + ROLE_NO_FIELD_TITLE_FLS + ); + + /** + * Example user in which {@link Song#FIELD_TITLE} field is masked. + */ + static final TestSecurityConfig.User USER_ONLY_FIELD_TITLE_MASKED = new TestSecurityConfig.User("masked_user").roles( + ROLE_ONLY_FIELD_TITLE_MASKED + ); + + /** + * Example user with fls filter in which the user can only see the {@link Song#FIELD_TITLE} field and can see every field but the {@link Song#FIELD_TITLE} field- should default to showing no fields. + */ + static final TestSecurityConfig.User USER_BOTH_ONLY_AND_NO_FIELD_TITLE_FLS = new TestSecurityConfig.User("inclusive_exclusive_fls_user") + .roles(ROLE_ONLY_FIELD_TITLE_FLS, ROLE_NO_FIELD_TITLE_FLS); + + /** + * Example user with fls filter in which the user can only see the {@link Song#FIELD_TITLE} field and in which {@link Song#FIELD_TITLE} field is masked. + */ + static final TestSecurityConfig.User USER_BOTH_ONLY_FIELD_TITLE_FLS_ONLY_FIELD_TITLE_MASKED = new TestSecurityConfig.User( + "inclusive_masked_user" + ).roles(ROLE_ONLY_FIELD_TITLE_FLS, ROLE_ONLY_FIELD_TITLE_MASKED); + + /** + * Example user with fls filter in which the user can see every field but the {@link Song#FIELD_TITLE} field and in which {@link Song#FIELD_TITLE} field is masked- {@link Song#FIELD_TITLE} field should not be visible. + */ + static final TestSecurityConfig.User USER_BOTH_NO_FIELD_TITLE_FLS_ONLY_FIELD_TITLE_MASKED = new TestSecurityConfig.User( + "exclusive_masked_user" + ).roles(ROLE_NO_FIELD_TITLE_FLS, ROLE_ONLY_FIELD_TITLE_MASKED); + + /** + * Example user with fls filter in which the user can only see the {@link Song#FIELD_TITLE} field and can see every field but the {@link Song#FIELD_TITLE} field and in which {@link Song#FIELD_TITLE} field is masked- should default to showing no fields. + */ + static final TestSecurityConfig.User USER_ALL_ONLY_AND_NO_FIELD_TITLE_FLS_ONLY_FIELD_TITLE_MASKED = new TestSecurityConfig.User( + "inclusive_exclusive_masked_user" + ).roles(ROLE_ONLY_FIELD_TITLE_FLS, ROLE_NO_FIELD_TITLE_FLS, ROLE_ONLY_FIELD_TITLE_MASKED); + @ClassRule public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) .anonymousAuth(false) @@ -228,7 +294,14 @@ public class FlsAndFieldMaskingTests { MASKED_ARTIST_LYRICS_READER, ALL_INDICES_STRING_ARTIST_READER, ALL_INDICES_STARS_LESS_THAN_ZERO_READER, - TWINS_FIRST_ARTIST_READER + TWINS_FIRST_ARTIST_READER, + USER_ONLY_FIELD_TITLE_FLS, + USER_NO_FIELD_TITLE_FLS, + USER_ONLY_FIELD_TITLE_MASKED, + USER_BOTH_ONLY_AND_NO_FIELD_TITLE_FLS, + USER_BOTH_ONLY_FIELD_TITLE_FLS_ONLY_FIELD_TITLE_MASKED, + USER_BOTH_NO_FIELD_TITLE_FLS_ONLY_FIELD_TITLE_MASKED, + USER_ALL_ONLY_AND_NO_FIELD_TITLE_FLS_ONLY_FIELD_TITLE_MASKED ) .build(); @@ -833,6 +906,819 @@ public void getFieldCapabilities() throws IOException { } @Test + public void testGetDocumentWithNoTitleFieldOrOnlyTitleFieldFLSRestrictions() throws IOException, Exception { + GetRequest getRequest = new GetRequest(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1); + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ONLY_FIELD_TITLE_FLS)) { + assertGetForFLSRestrictions(restHighLevelClient, getRequest, true); + } + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_NO_FIELD_TITLE_FLS)) { + assertGetForFLSRestrictions(restHighLevelClient, getRequest, false); + } + } + + private void assertGetForFLSRestrictions(RestHighLevelClient restHighLevelClient, GetRequest getRequest, boolean shouldShowFieldTitle) + throws IOException, Exception { + // if shouldShowFieldTitle == true, we check that only the title field is fetched; if shouldShowFieldTitle == false, we check that + // only the title field is + // ignored + GetResponse getResponse = restHighLevelClient.get(getRequest, DEFAULT); + + assertThat(getResponse, containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1)); + + Matcher containsTitleField = documentContainField( + FIELD_TITLE, + FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getTitle() + ); + Matcher containsArtistField = documentContainField( + FIELD_ARTIST, + FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getArtist() + ); + Matcher containsLyricsField = documentContainField( + FIELD_LYRICS, + FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getLyrics() + ); + Matcher containsStarsField = documentContainField( + FIELD_STARS, + FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getStars() + ); + Matcher containsGenreField = documentContainField( + FIELD_GENRE, + FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getGenre() + ); + + assertThat(getResponse, shouldShowFieldTitle ? containsTitleField : not(containsTitleField)); + assertThat(getResponse, shouldShowFieldTitle ? not(containsArtistField) : containsArtistField); + assertThat(getResponse, shouldShowFieldTitle ? not(containsLyricsField) : containsLyricsField); + assertThat(getResponse, shouldShowFieldTitle ? not(containsStarsField) : containsStarsField); + assertThat(getResponse, shouldShowFieldTitle ? not(containsGenreField) : containsGenreField); + } + + @Test + public void testMultiGetDocumentWithNoTitleFieldOrOnlyTitleFieldFLSRestrictions() throws IOException, Exception { + MultiGetRequest multiGetRequest = new MultiGetRequest(); + multiGetRequest.add(new MultiGetRequest.Item(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1)); + multiGetRequest.add(new MultiGetRequest.Item(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_2)); + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ONLY_FIELD_TITLE_FLS)) { + assertMGetForFLSRestrictions(restHighLevelClient, multiGetRequest, true); + } + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_NO_FIELD_TITLE_FLS)) { + assertMGetForFLSRestrictions(restHighLevelClient, multiGetRequest, false); + } + } + + private void assertMGetForFLSRestrictions( + RestHighLevelClient restHighLevelClient, + MultiGetRequest multiGetRequest, + boolean shouldShowFieldTitle + ) throws IOException, Exception { + // if shouldShowFieldTitle == true, we check that only the title field is fetched; if shouldShowFieldTitle == false, we check that + // only the title field is + // ignored + MultiGetResponse multiGetResponse = restHighLevelClient.mget(multiGetRequest, DEFAULT); + List getResponses = Arrays.stream(multiGetResponse.getResponses()) + .map(MultiGetItemResponse::getResponse) + .collect(Collectors.toList()); + + assertThat(getResponses, hasItem(containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1))); + assertThat(getResponses, hasItem(containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_2))); + + Matcher documentOneContainsTitleField = documentContainField( + FIELD_TITLE, + FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getTitle() + ); + Matcher documentOneContainsArtistField = documentContainField( + FIELD_ARTIST, + FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getArtist() + ); + Matcher documentOneContainsLyricsField = documentContainField( + FIELD_LYRICS, + FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getLyrics() + ); + Matcher documentOneContainsStarsField = documentContainField( + FIELD_STARS, + FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getStars() + ); + Matcher documentOneContainsGenreField = documentContainField( + FIELD_GENRE, + FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getGenre() + ); + Matcher documentTwoContainsTitleField = documentContainField( + FIELD_TITLE, + FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getTitle() + ); + Matcher documentTwoContainsArtistField = documentContainField( + FIELD_ARTIST, + FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getArtist() + ); + Matcher documentTwoContainsLyricsField = documentContainField( + FIELD_LYRICS, + FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getLyrics() + ); + Matcher documentTwoContainsStarsField = documentContainField( + FIELD_STARS, + FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getStars() + ); + Matcher documentTwoContainsGenreField = documentContainField( + FIELD_GENRE, + FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getGenre() + ); + + assertThat( + getResponses, + shouldShowFieldTitle ? hasItem(documentOneContainsTitleField) : not(hasItem(documentOneContainsTitleField)) + ); + assertThat( + getResponses, + shouldShowFieldTitle ? not(hasItem(documentOneContainsArtistField)) : hasItem(documentOneContainsArtistField) + ); + assertThat( + getResponses, + shouldShowFieldTitle ? not(hasItem(documentOneContainsLyricsField)) : hasItem(documentOneContainsLyricsField) + ); + assertThat( + getResponses, + shouldShowFieldTitle ? not(hasItem(documentOneContainsStarsField)) : hasItem(documentOneContainsStarsField) + ); + assertThat( + getResponses, + shouldShowFieldTitle ? not(hasItem(documentOneContainsGenreField)) : hasItem(documentOneContainsGenreField) + ); + assertThat( + getResponses, + shouldShowFieldTitle ? hasItem(documentTwoContainsTitleField) : not(hasItem(documentTwoContainsTitleField)) + ); + assertThat( + getResponses, + shouldShowFieldTitle ? not(hasItem(documentTwoContainsArtistField)) : hasItem(documentTwoContainsArtistField) + ); + assertThat( + getResponses, + shouldShowFieldTitle ? not(hasItem(documentTwoContainsLyricsField)) : hasItem(documentTwoContainsLyricsField) + ); + assertThat( + getResponses, + shouldShowFieldTitle ? not(hasItem(documentTwoContainsStarsField)) : hasItem(documentTwoContainsStarsField) + ); + assertThat( + getResponses, + shouldShowFieldTitle ? not(hasItem(documentTwoContainsGenreField)) : hasItem(documentTwoContainsGenreField) + ); + } + + @Test + public void testSearchDocumentWithWithNoTitleFieldOrOnlyTitleFieldFLSRestrictions() throws IOException, Exception { + SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ONLY_FIELD_TITLE_FLS)) { + assertSearchForFLSRestrictions(restHighLevelClient, searchRequest, true); + } + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_NO_FIELD_TITLE_FLS)) { + assertSearchForFLSRestrictions(restHighLevelClient, searchRequest, false); + } + } + + private void assertSearchForFLSRestrictions( + RestHighLevelClient restHighLevelClient, + SearchRequest searchRequest, + boolean shouldShowFieldTitle + ) throws IOException, Exception { + // if shouldShowFieldTitle == true, we check that only the title field is fetched; if shouldShowFieldTitle == false, we check that + // only the title field is + // ignored + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(4)); + + IntStream.range(0, 4).forEach(hitIndex -> { + assertThat( + searchResponse, + shouldShowFieldTitle + ? searchHitContainsFieldWithValue(hitIndex, FIELD_TITLE, SONGS[hitIndex].getTitle()) + : searchHitDoesNotContainField(hitIndex, FIELD_TITLE) + ); + assertThat( + searchResponse, + shouldShowFieldTitle + ? searchHitDoesNotContainField(hitIndex, FIELD_ARTIST) + : searchHitContainsFieldWithValue(hitIndex, FIELD_ARTIST, SONGS[hitIndex].getArtist()) + ); + assertThat( + searchResponse, + shouldShowFieldTitle + ? searchHitDoesNotContainField(hitIndex, FIELD_LYRICS) + : searchHitContainsFieldWithValue(hitIndex, FIELD_LYRICS, SONGS[hitIndex].getLyrics()) + ); + assertThat( + searchResponse, + shouldShowFieldTitle + ? searchHitDoesNotContainField(hitIndex, FIELD_STARS) + : searchHitContainsFieldWithValue(hitIndex, FIELD_STARS, SONGS[hitIndex].getStars()) + ); + assertThat( + searchResponse, + shouldShowFieldTitle + ? searchHitDoesNotContainField(hitIndex, FIELD_GENRE) + : searchHitContainsFieldWithValue(hitIndex, FIELD_GENRE, SONGS[hitIndex].getGenre()) + ); + }); + } + + @Test + public void testGetDocumentWithTitleFieldMaskingRestriction() throws IOException, Exception { + GetRequest getRequest = new GetRequest(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1); + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ONLY_FIELD_TITLE_MASKED)) { + assertProperGetResponsesForTitleFieldMaskingRestriction(restHighLevelClient, getRequest); + } + } + + private void assertProperGetResponsesForTitleFieldMaskingRestriction(RestHighLevelClient restHighLevelClient, GetRequest getRequest) + throws IOException, Exception { + GetResponse getResponse = restHighLevelClient.get(getRequest, DEFAULT); + + assertThat(getResponse, containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1)); + assertThat( + getResponse, + documentContainField(FIELD_TITLE, VALUE_TO_MASKED_VALUE.apply(FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getTitle())) + ); + assertThat(getResponse, documentContainField(FIELD_ARTIST, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getArtist())); + assertThat(getResponse, documentContainField(FIELD_LYRICS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getLyrics())); + assertThat(getResponse, documentContainField(FIELD_STARS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getStars())); + assertThat(getResponse, documentContainField(FIELD_GENRE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getGenre())); + } + + @Test + public void testMultiGetDocumentWithTitleFieldMaskingRestriction() throws IOException, Exception { + MultiGetRequest multiGetRequest = new MultiGetRequest(); + multiGetRequest.add(new MultiGetRequest.Item(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1)); + multiGetRequest.add(new MultiGetRequest.Item(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_2)); + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ONLY_FIELD_TITLE_MASKED)) { + assertProperMultiGetResponseForTitleFieldMaskingRestriction(restHighLevelClient, multiGetRequest); + } + } + + private void assertProperMultiGetResponseForTitleFieldMaskingRestriction( + RestHighLevelClient restHighLevelClient, + MultiGetRequest multiGetRequest + ) throws IOException, Exception { + MultiGetResponse multiGetResponse = restHighLevelClient.mget(multiGetRequest, DEFAULT); + List getResponses = Arrays.stream(multiGetResponse.getResponses()) + .map(MultiGetItemResponse::getResponse) + .collect(Collectors.toList()); + + assertThat(getResponses, hasItem(containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1))); + assertThat(getResponses, hasItem(containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_2))); + assertThat( + getResponses, + hasItem( + documentContainField( + FIELD_TITLE, + VALUE_TO_MASKED_VALUE.apply(FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getTitle()) + ) + ) + ); + assertThat( + getResponses, + hasItem( + documentContainField( + FIELD_TITLE, + VALUE_TO_MASKED_VALUE.apply(FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getTitle()) + ) + ) + ); + assertThat( + getResponses, + hasItem(documentContainField(FIELD_ARTIST, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getArtist())) + ); + assertThat( + getResponses, + hasItem(documentContainField(FIELD_ARTIST, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getArtist())) + ); + assertThat( + getResponses, + hasItem(documentContainField(FIELD_LYRICS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getLyrics())) + ); + assertThat( + getResponses, + hasItem(documentContainField(FIELD_LYRICS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getLyrics())) + ); + assertThat(getResponses, hasItem(documentContainField(FIELD_STARS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getStars()))); + assertThat(getResponses, hasItem(documentContainField(FIELD_STARS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getStars()))); + assertThat(getResponses, hasItem(documentContainField(FIELD_GENRE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getGenre()))); + assertThat(getResponses, hasItem(documentContainField(FIELD_GENRE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getGenre()))); + } + + @Test + public void testSearchDocumentWithTitleFieldMaskingRestriction() throws IOException, Exception { + SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ONLY_FIELD_TITLE_MASKED)) { + assertProperSearchResponseForTitleFieldMaskingRestriction(restHighLevelClient, searchRequest); + } + } + + private void assertProperSearchResponseForTitleFieldMaskingRestriction( + RestHighLevelClient restHighLevelClient, + SearchRequest searchRequest + ) throws IOException, Exception { + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(4)); + IntStream.range(0, 4).forEach(hitIndex -> { + assertThat( + searchResponse, + searchHitContainsFieldWithValue(hitIndex, FIELD_TITLE, VALUE_TO_MASKED_VALUE.apply(SONGS[hitIndex].getTitle())) + ); + assertThat(searchResponse, searchHitContainsFieldWithValue(hitIndex, FIELD_ARTIST, SONGS[hitIndex].getArtist())); + assertThat(searchResponse, searchHitContainsFieldWithValue(hitIndex, FIELD_LYRICS, SONGS[hitIndex].getLyrics())); + assertThat(searchResponse, searchHitContainsFieldWithValue(hitIndex, FIELD_STARS, SONGS[hitIndex].getStars())); + assertThat(searchResponse, searchHitContainsFieldWithValue(hitIndex, FIELD_GENRE, SONGS[hitIndex].getGenre())); + }); + } + + @Test + public void testGetDocumentWithNoTitleFieldAndOnlyTitleFieldFLSRestrictions() throws IOException, Exception { + GetRequest getRequest = new GetRequest(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1); + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_BOTH_ONLY_AND_NO_FIELD_TITLE_FLS)) { + assertProperGetResponsesForOnlyAndNoTitleFLSRestrictions(restHighLevelClient, getRequest); + } + } + + private void assertProperGetResponsesForOnlyAndNoTitleFLSRestrictions(RestHighLevelClient restHighLevelClient, GetRequest getRequest) + throws IOException, Exception { + GetResponse getResponse = restHighLevelClient.get(getRequest, DEFAULT); + + assertThat(getResponse, containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1)); + + // since the roles are overlapping, the role with less permissions is the only one that is used- which is no title + assertThat(getResponse, not(documentContainField(FIELD_TITLE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getTitle()))); + assertThat(getResponse, documentContainField(FIELD_ARTIST, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getArtist())); + assertThat(getResponse, documentContainField(FIELD_LYRICS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getLyrics())); + assertThat(getResponse, documentContainField(FIELD_STARS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getStars())); + assertThat(getResponse, documentContainField(FIELD_GENRE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getGenre())); + } + + @Test + public void testMultiGetDocumentWithNoTitleFieldAndOnlyTitleFieldFLSRestrictions() throws IOException, Exception { + MultiGetRequest multiGetRequest = new MultiGetRequest(); + multiGetRequest.add(new MultiGetRequest.Item(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1)); + multiGetRequest.add(new MultiGetRequest.Item(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_2)); + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_BOTH_ONLY_AND_NO_FIELD_TITLE_FLS)) { + assertProperMultiGetResponseForOnlyAndNoTitleFLSRestrictions(restHighLevelClient, multiGetRequest); + } + } + + private void assertProperMultiGetResponseForOnlyAndNoTitleFLSRestrictions( + RestHighLevelClient restHighLevelClient, + MultiGetRequest multiGetRequest + ) throws IOException, Exception { + MultiGetResponse multiGetResponse = restHighLevelClient.mget(multiGetRequest, DEFAULT); + List getResponses = Arrays.stream(multiGetResponse.getResponses()) + .map(MultiGetItemResponse::getResponse) + .collect(Collectors.toList()); + + assertThat(getResponses, hasItem(containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1))); + assertThat(getResponses, hasItem(containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_2))); + + // since the roles are overlapping, the role with less permissions is the only one that is used- which is no title + assertThat( + getResponses, + not(hasItem(documentContainField(FIELD_TITLE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getTitle()))) + ); + assertThat( + getResponses, + hasItem(documentContainField(FIELD_ARTIST, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getArtist())) + ); + assertThat( + getResponses, + hasItem(documentContainField(FIELD_LYRICS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getLyrics())) + ); + assertThat(getResponses, hasItem(documentContainField(FIELD_STARS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getStars()))); + assertThat(getResponses, hasItem(documentContainField(FIELD_GENRE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getGenre()))); + assertThat( + getResponses, + not(hasItem(documentContainField(FIELD_TITLE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getTitle()))) + ); + assertThat( + getResponses, + hasItem(documentContainField(FIELD_ARTIST, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getArtist())) + ); + assertThat( + getResponses, + hasItem(documentContainField(FIELD_LYRICS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getLyrics())) + ); + assertThat(getResponses, hasItem(documentContainField(FIELD_STARS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getStars()))); + assertThat(getResponses, hasItem(documentContainField(FIELD_GENRE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getGenre()))); + } + + @Test + public void testSearchDocumentWithWithNoTitleFieldAndOnlyTitleFieldFLSRestrictions() throws IOException, Exception { + SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_BOTH_ONLY_AND_NO_FIELD_TITLE_FLS)) { + assertProperSearchResponseForOnlyAndNoTitleFLSRestrictions(restHighLevelClient, searchRequest); + } + } + + private void assertProperSearchResponseForOnlyAndNoTitleFLSRestrictions( + RestHighLevelClient restHighLevelClient, + SearchRequest searchRequest + ) throws IOException, Exception { + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(4)); + + // since the roles are overlapping, the role with less permissions is the only one that is used- which is no title + IntStream.range(0, 4).forEach(hitIndex -> { + assertThat(searchResponse, searchHitDoesNotContainField(hitIndex, FIELD_TITLE)); + assertThat(searchResponse, searchHitDoesContainField(hitIndex, FIELD_ARTIST)); + assertThat(searchResponse, searchHitDoesContainField(hitIndex, FIELD_LYRICS)); + assertThat(searchResponse, searchHitDoesContainField(hitIndex, FIELD_STARS)); + assertThat(searchResponse, searchHitDoesContainField(hitIndex, FIELD_GENRE)); + }); + } + + @Test + public void testGetDocumentWithTitleFieldMaskingAndOnlyTitleFLSRestrictions() throws IOException, Exception { + GetRequest getRequest = new GetRequest(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_BOTH_ONLY_FIELD_TITLE_FLS_ONLY_FIELD_TITLE_MASKED) + ) { + assertProperGetResponsesForTitleFieldMaskingAndOnlyTitleFLSRestrictions(restHighLevelClient, getRequest); + } + } + + private void assertProperGetResponsesForTitleFieldMaskingAndOnlyTitleFLSRestrictions( + RestHighLevelClient restHighLevelClient, + GetRequest getRequest + ) throws IOException, Exception { + GetResponse getResponse = restHighLevelClient.get(getRequest, DEFAULT); + + assertThat(getResponse, containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1)); + assertThat( + getResponse, + documentContainField(FIELD_TITLE, VALUE_TO_MASKED_VALUE.apply(FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getTitle())) + ); + assertThat(getResponse, not(documentContainField(FIELD_ARTIST, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getArtist()))); + assertThat(getResponse, not(documentContainField(FIELD_LYRICS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getLyrics()))); + assertThat(getResponse, not(documentContainField(FIELD_STARS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getStars()))); + assertThat(getResponse, not(documentContainField(FIELD_GENRE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getGenre()))); + } + + @Test + public void testMultiGetDocumentWithTitleFieldMaskingAndOnlyTitleFLSRestrictions() throws IOException, Exception { + MultiGetRequest multiGetRequest = new MultiGetRequest(); + multiGetRequest.add(new MultiGetRequest.Item(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1)); + multiGetRequest.add(new MultiGetRequest.Item(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_2)); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_BOTH_ONLY_FIELD_TITLE_FLS_ONLY_FIELD_TITLE_MASKED) + ) { + assertProperMultiGetResponseForTitleFieldMaskingAndOnlyTitleFLSRestrictions(restHighLevelClient, multiGetRequest); + } + } + + private void assertProperMultiGetResponseForTitleFieldMaskingAndOnlyTitleFLSRestrictions( + RestHighLevelClient restHighLevelClient, + MultiGetRequest multiGetRequest + ) throws IOException, Exception { + MultiGetResponse multiGetResponse = restHighLevelClient.mget(multiGetRequest, DEFAULT); + List getResponses = Arrays.stream(multiGetResponse.getResponses()) + .map(MultiGetItemResponse::getResponse) + .collect(Collectors.toList()); + + assertThat(getResponses, hasItem(containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1))); + assertThat(getResponses, hasItem(containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_2))); + assertThat( + getResponses, + hasItem( + documentContainField( + FIELD_TITLE, + VALUE_TO_MASKED_VALUE.apply(FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getTitle()) + ) + ) + ); + assertThat( + getResponses, + hasItem( + documentContainField( + FIELD_TITLE, + VALUE_TO_MASKED_VALUE.apply(FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getTitle()) + ) + ) + ); + assertThat( + getResponses, + not(hasItem(documentContainField(FIELD_ARTIST, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getArtist()))) + ); + assertThat( + getResponses, + not(hasItem(documentContainField(FIELD_ARTIST, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getArtist()))) + ); + assertThat( + getResponses, + not(hasItem(documentContainField(FIELD_LYRICS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getLyrics()))) + ); + assertThat( + getResponses, + not(hasItem(documentContainField(FIELD_LYRICS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getLyrics()))) + ); + assertThat( + getResponses, + not(hasItem(documentContainField(FIELD_STARS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getStars()))) + ); + assertThat( + getResponses, + not(hasItem(documentContainField(FIELD_STARS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getStars()))) + ); + assertThat( + getResponses, + not(hasItem(documentContainField(FIELD_GENRE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getGenre()))) + ); + assertThat( + getResponses, + not(hasItem(documentContainField(FIELD_GENRE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getGenre()))) + ); + } + + @Test + public void testSearchDocumentWithTitleFieldMaskingAndOnlyTitleFLSRestrictions() throws IOException, Exception { + SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_BOTH_ONLY_FIELD_TITLE_FLS_ONLY_FIELD_TITLE_MASKED) + ) { + assertProperSearchResponseForTitleFieldMaskingAndOnlyTitleFLSRestrictions(restHighLevelClient, searchRequest); + } + } + + private void assertProperSearchResponseForTitleFieldMaskingAndOnlyTitleFLSRestrictions( + RestHighLevelClient restHighLevelClient, + SearchRequest searchRequest + ) throws IOException, Exception { + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(4)); + IntStream.range(0, 4).forEach(hitIndex -> { + assertThat( + searchResponse, + searchHitContainsFieldWithValue(hitIndex, FIELD_TITLE, VALUE_TO_MASKED_VALUE.apply(SONGS[hitIndex].getTitle())) + ); + assertThat(searchResponse, searchHitDoesNotContainField(hitIndex, FIELD_ARTIST)); + assertThat(searchResponse, searchHitDoesNotContainField(hitIndex, FIELD_LYRICS)); + assertThat(searchResponse, searchHitDoesNotContainField(hitIndex, FIELD_STARS)); + assertThat(searchResponse, searchHitDoesNotContainField(hitIndex, FIELD_GENRE)); + }); + } + + @Test + public void testGetDocumentWithTitleFieldMaskingAndNoTitleFLSRestrictions() throws IOException, Exception { + GetRequest getRequest = new GetRequest(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_BOTH_NO_FIELD_TITLE_FLS_ONLY_FIELD_TITLE_MASKED) + ) { + assertProperGetResponsesForTitleFieldMaskingAndNoTitleFLSRestrictions(restHighLevelClient, getRequest); + } + } + + private void assertProperGetResponsesForTitleFieldMaskingAndNoTitleFLSRestrictions( + RestHighLevelClient restHighLevelClient, + GetRequest getRequest + ) throws IOException, Exception { + GetResponse getResponse = restHighLevelClient.get(getRequest, DEFAULT); + + assertThat(getResponse, containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1)); + assertThat(getResponse, not(documentContainField(FIELD_TITLE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getTitle()))); + assertThat(getResponse, documentContainField(FIELD_ARTIST, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getArtist())); + assertThat(getResponse, documentContainField(FIELD_LYRICS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getLyrics())); + assertThat(getResponse, documentContainField(FIELD_STARS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getStars())); + assertThat(getResponse, documentContainField(FIELD_GENRE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getGenre())); + } + + @Test + public void testMultiGetDocumentWithTitleFieldMaskingAndNoTitleFLSRestrictions() throws IOException, Exception { + MultiGetRequest multiGetRequest = new MultiGetRequest(); + multiGetRequest.add(new MultiGetRequest.Item(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1)); + multiGetRequest.add(new MultiGetRequest.Item(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_2)); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_BOTH_NO_FIELD_TITLE_FLS_ONLY_FIELD_TITLE_MASKED) + ) { + assertProperMultiGetResponseForTitleFieldMaskingAndNoTitleFLSRestrictions(restHighLevelClient, multiGetRequest); + } + } + + private void assertProperMultiGetResponseForTitleFieldMaskingAndNoTitleFLSRestrictions( + RestHighLevelClient restHighLevelClient, + MultiGetRequest multiGetRequest + ) throws IOException, Exception { + MultiGetResponse multiGetResponse = restHighLevelClient.mget(multiGetRequest, DEFAULT); + List getResponses = Arrays.stream(multiGetResponse.getResponses()) + .map(MultiGetItemResponse::getResponse) + .collect(Collectors.toList()); + + assertThat(getResponses, hasItem(containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1))); + assertThat(getResponses, hasItem(containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_2))); + assertThat( + getResponses, + not(hasItem(documentContainField(FIELD_TITLE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getTitle()))) + ); + assertThat( + getResponses, + not(hasItem(documentContainField(FIELD_TITLE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getTitle()))) + ); + assertThat( + getResponses, + hasItem(documentContainField(FIELD_ARTIST, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getArtist())) + ); + assertThat( + getResponses, + hasItem(documentContainField(FIELD_ARTIST, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getArtist())) + ); + assertThat( + getResponses, + hasItem(documentContainField(FIELD_LYRICS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getLyrics())) + ); + assertThat( + getResponses, + hasItem(documentContainField(FIELD_LYRICS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getLyrics())) + ); + assertThat(getResponses, hasItem(documentContainField(FIELD_STARS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getStars()))); + assertThat(getResponses, hasItem(documentContainField(FIELD_STARS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getStars()))); + assertThat(getResponses, hasItem(documentContainField(FIELD_GENRE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getGenre()))); + assertThat(getResponses, hasItem(documentContainField(FIELD_GENRE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getGenre()))); + } + + @Test + public void testSearchDocumentWithTitleFieldMaskingAndNoTitleFLSRestrictions() throws IOException, Exception { + SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_BOTH_NO_FIELD_TITLE_FLS_ONLY_FIELD_TITLE_MASKED) + ) { + assertProperSearchResponseForTitleFieldMaskingAndNoTitleFLSRestrictions(restHighLevelClient, searchRequest); + } + } + + private void assertProperSearchResponseForTitleFieldMaskingAndNoTitleFLSRestrictions( + RestHighLevelClient restHighLevelClient, + SearchRequest searchRequest + ) throws IOException, Exception { + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(4)); + IntStream.range(0, 4).forEach(hitIndex -> { + assertThat(searchResponse, searchHitDoesNotContainField(hitIndex, FIELD_TITLE)); + assertThat(searchResponse, searchHitContainsFieldWithValue(hitIndex, FIELD_ARTIST, SONGS[hitIndex].getArtist())); + assertThat(searchResponse, searchHitContainsFieldWithValue(hitIndex, FIELD_LYRICS, SONGS[hitIndex].getLyrics())); + assertThat(searchResponse, searchHitContainsFieldWithValue(hitIndex, FIELD_STARS, SONGS[hitIndex].getStars())); + assertThat(searchResponse, searchHitContainsFieldWithValue(hitIndex, FIELD_GENRE, SONGS[hitIndex].getGenre())); + }); + } + + @Test + public void testGetDocumentWithTitleFieldMaskingAndNoTitleFieldAndOnlyTitleFieldFLSRestrictions() throws IOException, Exception { + GetRequest getRequest = new GetRequest(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALL_ONLY_AND_NO_FIELD_TITLE_FLS_ONLY_FIELD_TITLE_MASKED + ) + ) { + assertProperGetResponsesForTitleFieldMaskingAndNoTitleFieldAndOnlyTitleFieldFLSRestrictions(restHighLevelClient, getRequest); + } + } + + private void assertProperGetResponsesForTitleFieldMaskingAndNoTitleFieldAndOnlyTitleFieldFLSRestrictions( + RestHighLevelClient restHighLevelClient, + GetRequest getRequest + ) throws IOException, Exception { + GetResponse getResponse = restHighLevelClient.get(getRequest, DEFAULT); + + assertThat(getResponse, containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1)); + + // since the roles are overlapping, the role with less permissions is the only one that is used- which is no title, and since there + // is no title the masking role has no effect + assertThat(getResponse, not(documentContainField(FIELD_TITLE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getTitle()))); + assertThat(getResponse, documentContainField(FIELD_ARTIST, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getArtist())); + assertThat(getResponse, documentContainField(FIELD_LYRICS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getLyrics())); + assertThat(getResponse, documentContainField(FIELD_STARS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getStars())); + assertThat(getResponse, documentContainField(FIELD_GENRE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getGenre())); + } + + @Test + public void testMultiGetDocumentWithTitleFieldMaskingAndNoTitleFieldAndOnlyTitleFieldFLSRestrictions() throws IOException, Exception { + MultiGetRequest multiGetRequest = new MultiGetRequest(); + multiGetRequest.add(new MultiGetRequest.Item(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1)); + multiGetRequest.add(new MultiGetRequest.Item(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_2)); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALL_ONLY_AND_NO_FIELD_TITLE_FLS_ONLY_FIELD_TITLE_MASKED + ) + ) { + assertProperMultiGetResponseForTitleFieldMaskingAndNoTitleFieldAndOnlyTitleFieldFLSRestrictions( + restHighLevelClient, + multiGetRequest + ); + } + } + + private void assertProperMultiGetResponseForTitleFieldMaskingAndNoTitleFieldAndOnlyTitleFieldFLSRestrictions( + RestHighLevelClient restHighLevelClient, + MultiGetRequest multiGetRequest + ) throws IOException, Exception { + MultiGetResponse multiGetResponse = restHighLevelClient.mget(multiGetRequest, DEFAULT); + List getResponses = Arrays.stream(multiGetResponse.getResponses()) + .map(MultiGetItemResponse::getResponse) + .collect(Collectors.toList()); + + assertThat(getResponses, hasItem(containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1))); + assertThat(getResponses, hasItem(containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_2))); + + // since the roles are overlapping, the role with less permissions is the only one that is used- which is no title, and since there + // is no title the masking role has no effect + assertThat( + getResponses, + not(hasItem(documentContainField(FIELD_TITLE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getTitle()))) + ); + assertThat( + getResponses, + hasItem(documentContainField(FIELD_ARTIST, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getArtist())) + ); + assertThat( + getResponses, + hasItem(documentContainField(FIELD_LYRICS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getLyrics())) + ); + assertThat(getResponses, hasItem(documentContainField(FIELD_STARS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getStars()))); + assertThat(getResponses, hasItem(documentContainField(FIELD_GENRE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_1).getGenre()))); + assertThat( + getResponses, + not(hasItem(documentContainField(FIELD_TITLE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getTitle()))) + ); + assertThat( + getResponses, + hasItem(documentContainField(FIELD_ARTIST, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getArtist())) + ); + assertThat( + getResponses, + hasItem(documentContainField(FIELD_LYRICS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getLyrics())) + ); + assertThat(getResponses, hasItem(documentContainField(FIELD_STARS, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getStars()))); + assertThat(getResponses, hasItem(documentContainField(FIELD_GENRE, FIRST_INDEX_SONGS_BY_ID.get(FIRST_INDEX_ID_SONG_2).getGenre()))); + } + + @Test + public void testSearchDocumentWithTitleFieldMaskingAndNoTitleFieldAndOnlyTitleFieldFLSRestrictions() throws IOException, Exception { + SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALL_ONLY_AND_NO_FIELD_TITLE_FLS_ONLY_FIELD_TITLE_MASKED + ) + ) { + assertProperSearchResponseForTitleFieldMaskingAndNoTitleFieldAndOnlyTitleFieldFLSRestrictions( + restHighLevelClient, + searchRequest + ); + } + } + + private void assertProperSearchResponseForTitleFieldMaskingAndNoTitleFieldAndOnlyTitleFieldFLSRestrictions( + RestHighLevelClient restHighLevelClient, + SearchRequest searchRequest + ) throws IOException, Exception { + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(4)); + + // since the roles are overlapping, the role with less permissions is the only one that is used- which is no title, and since there + // is no title the masking role has no effect + IntStream.range(0, 4).forEach(hitIndex -> { + assertThat(searchResponse, searchHitDoesNotContainField(hitIndex, FIELD_TITLE)); + assertThat(searchResponse, searchHitDoesContainField(hitIndex, FIELD_ARTIST)); + assertThat(searchResponse, searchHitDoesContainField(hitIndex, FIELD_LYRICS)); + assertThat(searchResponse, searchHitDoesContainField(hitIndex, FIELD_STARS)); + assertThat(searchResponse, searchHitDoesContainField(hitIndex, FIELD_GENRE)); + }); + } + public void flsWithIncludesRulesIncludesFieldMappersFromPlugins() throws IOException { String indexName = "fls_includes_index"; TestSecurityConfig.Role userRole = new TestSecurityConfig.Role("fls_include_stars_reader").clusterPermissions(