diff --git a/dlic-dlsfls/src/main/java/com/floragunn/searchguard/dlsfls/DlsFlsValveImpl.java b/dlic-dlsfls/src/main/java/com/floragunn/searchguard/dlsfls/DlsFlsValveImpl.java index 148e422ea..f13b40a43 100644 --- a/dlic-dlsfls/src/main/java/com/floragunn/searchguard/dlsfls/DlsFlsValveImpl.java +++ b/dlic-dlsfls/src/main/java/com/floragunn/searchguard/dlsfls/DlsFlsValveImpl.java @@ -11,6 +11,22 @@ * from https://floragunn.com * */ +/* + * Includes parts from https://github.com/opensearch-project/security/blob/c18a50ac4c5f7116e0e7c3411944d1438f9c44e9/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java + * + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ package com.floragunn.searchguard.dlsfls; @@ -196,7 +212,15 @@ public boolean invoke(User user, ImmutableSet mappedRoles, String action if (request instanceof SearchRequest) { SearchRequest searchRequest = ((SearchRequest) request); - + if (searchRequest.source() != null && searchRequest.source().aggregations() != null) { + for (AggregationBuilder factory : searchRequest.source().aggregations().getAggregatorFactories()) { + if (factory instanceof TermsAggregationBuilder && ((TermsAggregationBuilder) factory).minDocCount() == 0) { + listener.onFailure(new ElasticsearchSecurityException("min_doc_count 0 is not supported when DLS is activated")); + return false; + } + } + } + //When we encounter a terms or sampler aggregation with masked fields activated we forcibly //need to switch off global ordinals because field masking can break ordering //https://www.elastic.co/guide/en/elasticsearch/reference/master/eager-global-ordinals.html#_avoiding_global_ordinal_loading diff --git a/dlic-dlsfls/src/test/java/com/floragunn/searchguard/dlic/dlsfls/DlsTest.java b/dlic-dlsfls/src/test/java/com/floragunn/searchguard/dlic/dlsfls/DlsTest.java index f900459fd..f10e9f5fc 100644 --- a/dlic-dlsfls/src/test/java/com/floragunn/searchguard/dlic/dlsfls/DlsTest.java +++ b/dlic-dlsfls/src/test/java/com/floragunn/searchguard/dlic/dlsfls/DlsTest.java @@ -11,10 +11,27 @@ * from https://floragunn.com * */ +/* + * Includes parts from https://github.com/opensearch-project/security/blob/c18a50ac4c5f7116e0e7c3411944d1438f9c44e9/src/test/java/org/opensearch/security/dlic/dlsfls/DlsTest.java + * + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ package com.floragunn.searchguard.dlic.dlsfls; import org.apache.http.HttpStatus; +import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; import org.elasticsearch.client.Client; @@ -24,6 +41,7 @@ import org.junit.ClassRule; import org.junit.Test; +import com.floragunn.fluent.collections.ImmutableMap; import com.floragunn.searchguard.test.GenericRestClient; import com.floragunn.searchguard.test.GenericRestClient.HttpResponse; import com.floragunn.searchguard.test.helper.cluster.JavaSecurityTestSetup; @@ -234,4 +252,250 @@ public void testDlsCache() throws Exception { Assert.assertTrue(res.getBody().contains("\"found\" : false")); } } + + @Test + public void testDlsWithMinDocCountZeroAggregations() throws Exception { + + try (Client client = cluster.getInternalNodeClient()) { + client.admin().indices().create(new CreateIndexRequest("logs").mapping("_doc", + ImmutableMap.of("properties", ImmutableMap.of("termX", ImmutableMap.of("type", "keyword"))))).actionGet(); + + for (int i = 0; i < 3; i++) { + client.index(new IndexRequest("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("amount", i, "termX", "A", "timestamp", + "2022-01-06T09:05:00Z")).actionGet(); + client.index(new IndexRequest("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("amount", i, "termX", "B", "timestamp", + "2022-01-06T09:08:00Z")).actionGet(); + client.index(new IndexRequest("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("amount", i, "termX", "C", "timestamp", + "2022-01-06T09:09:00Z")).actionGet(); + client.index(new IndexRequest("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("amount", i, "termX", "D", "timestamp", + "2022-01-06T09:10:00Z")).actionGet(); + } + client.index(new IndexRequest("logs").setRefreshPolicy(RefreshPolicy.IMMEDIATE).source("amount", 0, "termX", "E", "timestamp", + "2022-01-06T09:11:00Z")).actionGet(); + } + + try (GenericRestClient dmClient = cluster.getRestClient("dept_manager", "password"); + GenericRestClient adminClient = cluster.getRestClient("admin", "admin")) { + // Terms Aggregation + // Non-admin user with setting "min_doc_count":0. Expected to get error message "min_doc_count 0 is not supported when DLS is activated". + String query1 = "{\n"// + + " \"size\":0,\n"// + + " \"query\":{\n"// + + " \"bool\":{\n"// + + " \"must\":[\n"// + + " {\n"// + + " \"range\":{\n"// + + " \"amount\":{\"gte\":1,\"lte\":100}\n"// + + " }\n"// + + " }\n"// + + " ]\n"// + + " }\n"// + + " },\n"// + + " \"aggs\":{\n"// + + " \"a\": {\n"// + + " \"terms\": {\n"// + + " \"field\": \"termX\",\n"// + + " \"min_doc_count\":0,\n"// + + "\"size\": 10,\n"// + + "\"order\": { \"_count\": \"desc\" }\n"// + + " }\n"// + + " }\n"// + + " }\n"// + + "}"; + + HttpResponse response1 = dmClient.postJson("logs*/_search", query1); + + Assert.assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, response1.getStatusCode()); + Assert.assertTrue(response1.getBody(), response1.getBody().contains("min_doc_count 0 is not supported when DLS is activated")); + + // Non-admin user without setting "min_doc_count". Expected to only have access to buckets for dept_manager excluding E with 0 doc_count". + String query2 = "{\n"// + + " \"size\":0,\n"// + + " \"query\":{\n"// + + " \"bool\":{\n"// + + " \"must\":[\n"// + + " {\n"// + + " \"range\":{\n"// + + " \"amount\":{\"gte\":1,\"lte\":100}\n"// + + " }\n"// + + " }\n"// + + " ]\n"// + + " }\n"// + + " },\n"// + + " \"aggs\":{\n"// + + " \"a\": {\n"// + + " \"terms\": {\n"// + + " \"field\": \"termX\",\n"// + + "\"size\": 10,\n"// + + "\"order\": { \"_count\": \"desc\" }\n"// + + " }\n"// + + " }\n"// + + " }\n"// + + "}"; + + HttpResponse response2 = dmClient.postJson("logs*/_search", query2); + + Assert.assertEquals(HttpStatus.SC_OK, response2.getStatusCode()); + Assert.assertTrue(response2.getBody(), response2.getBody().contains("\"key\":\"A\"")); + Assert.assertFalse(response2.getBody(), response2.getBody().contains("\"key\":\"B\"")); + Assert.assertFalse(response2.getBody(), response2.getBody().contains("\"key\":\"C\"")); + Assert.assertFalse(response2.getBody(), response2.getBody().contains("\"key\":\"D\"")); + Assert.assertFalse(response2.getBody(), response2.getBody().contains("\"key\":\"E\"")); + + // Admin with setting "min_doc_count":0. Expected to have access to all buckets". + HttpResponse response3 = adminClient.postJson("logs*/_search", query1); + + Assert.assertEquals(HttpStatus.SC_OK, response3.getStatusCode()); + Assert.assertTrue(response3.getBody(), response3.getBody().contains("\"key\":\"A\"")); + Assert.assertTrue(response3.getBody(), response3.getBody().contains("\"key\":\"B\"")); + Assert.assertTrue(response3.getBody(), response3.getBody().contains("\"key\":\"C\"")); + Assert.assertTrue(response3.getBody(), response3.getBody().contains("\"key\":\"D\"")); + Assert.assertTrue(response3.getBody(), response3.getBody().contains("\"key\":\"E\",\"doc_count\":0")); + + // Admin without setting "min_doc_count". Expected to have access to all buckets excluding E with 0 doc_count". + HttpResponse response4 = adminClient.postJson("logs*/_search", query2); + + Assert.assertEquals(HttpStatus.SC_OK, response4.getStatusCode()); + Assert.assertTrue(response4.getBody(), response4.getBody().contains("\"key\":\"A\"")); + Assert.assertTrue(response4.getBody(), response4.getBody().contains("\"key\":\"B\"")); + Assert.assertTrue(response4.getBody(), response4.getBody().contains("\"key\":\"C\"")); + Assert.assertTrue(response4.getBody(), response4.getBody().contains("\"key\":\"D\"")); + Assert.assertFalse(response4.getBody(), response4.getBody().contains("\"key\":\"E\"")); + + // Significant Text Aggregation is not impacted. + // Non-admin user with setting "min_doc_count=0". Expected to only have access to buckets for dept_manager". + String query3 = "{\"aggregations\":{\"significant_termX\":{\"significant_terms\":{\"field\":\"termX.keyword\",\"min_doc_count\":0}}}}"; + HttpResponse response5 = dmClient.postJson("logs*/_search", query3); + + Assert.assertEquals(HttpStatus.SC_OK, response5.getStatusCode()); + Assert.assertTrue(response5.getBody(), response5.getBody().contains("\"termX\":\"A\"")); + Assert.assertFalse(response5.getBody(), response5.getBody().contains("\"termX\":\"B\"")); + Assert.assertFalse(response5.getBody(), response5.getBody().contains("\"termX\":\"C\"")); + Assert.assertFalse(response5.getBody(), response5.getBody().contains("\"termX\":\"D\"")); + Assert.assertFalse(response5.getBody(), response5.getBody().contains("\"termX\":\"E\"")); + + // Non-admin user without setting "min_doc_count". Expected to only have access to buckets for dept_manager". + String query4 = "{\"aggregations\":{\"significant_termX\":{\"significant_terms\":{\"field\":\"termX.keyword\"}}}}"; + + HttpResponse response6 = dmClient.postJson("logs*/_search", query4); + + Assert.assertEquals(HttpStatus.SC_OK, response6.getStatusCode()); + Assert.assertTrue(response6.getBody(), response6.getBody().contains("\"termX\":\"A\"")); + Assert.assertFalse(response6.getBody(), response6.getBody().contains("\"termX\":\"B\"")); + Assert.assertFalse(response6.getBody(), response6.getBody().contains("\"termX\":\"C\"")); + Assert.assertFalse(response6.getBody(), response6.getBody().contains("\"termX\":\"D\"")); + Assert.assertFalse(response6.getBody(), response6.getBody().contains("\"termX\":\"E\"")); + + // Admin with setting "min_doc_count":0. Expected to have access to all buckets". + HttpResponse response7 = adminClient.postJson("logs*/_search", query3); + + Assert.assertEquals(HttpStatus.SC_OK, response7.getStatusCode()); + Assert.assertTrue(response7.getBody(), response7.getBody().contains("\"termX\":\"A\"")); + Assert.assertTrue(response7.getBody(), response7.getBody().contains("\"termX\":\"B\"")); + Assert.assertTrue(response7.getBody(), response7.getBody().contains("\"termX\":\"C\"")); + Assert.assertTrue(response7.getBody(), response7.getBody().contains("\"termX\":\"D\"")); + Assert.assertTrue(response7.getBody(), response7.getBody().contains("\"termX\":\"E\"")); + + // Admin without setting "min_doc_count". Expected to have access to all buckets". + HttpResponse response8 = adminClient.postJson("logs*/_search", query4); + + Assert.assertEquals(HttpStatus.SC_OK, response8.getStatusCode()); + Assert.assertTrue(response8.getBody(), response8.getBody().contains("\"termX\":\"A\"")); + Assert.assertTrue(response8.getBody(), response8.getBody().contains("\"termX\":\"B\"")); + Assert.assertTrue(response8.getBody(), response8.getBody().contains("\"termX\":\"C\"")); + Assert.assertTrue(response8.getBody(), response8.getBody().contains("\"termX\":\"D\"")); + Assert.assertTrue(response8.getBody(), response8.getBody().contains("\"termX\":\"E\"")); + + // Histogram Aggregation is not impacted. + // Non-admin user with setting "min_doc_count=0". Expected to only have access to buckets for dept_manager". + String query5 = "{\"aggs\":{\"amount\":{\"histogram\":{\"field\":\"amount\",\"interval\":1,\"min_doc_count\":0}}}}"; + + HttpResponse response9 = dmClient.postJson("logs*/_search", query5); + + Assert.assertEquals(HttpStatus.SC_OK, response9.getStatusCode()); + Assert.assertTrue(response9.getBody(), response9.getBody().contains("\"termX\":\"A\"")); + Assert.assertFalse(response9.getBody(), response9.getBody().contains("\"termX\":\"B\"")); + Assert.assertFalse(response9.getBody(), response9.getBody().contains("\"termX\":\"C\"")); + Assert.assertFalse(response9.getBody(), response9.getBody().contains("\"termX\":\"D\"")); + Assert.assertFalse(response9.getBody(), response9.getBody().contains("\"termX\":\"E\"")); + + // Non-admin user without setting "min_doc_count". Expected to only have access to buckets for dept_manager". + String query6 = "{\"aggs\":{\"amount\":{\"histogram\":{\"field\":\"amount\",\"interval\":1}}}}"; + + HttpResponse response10 = dmClient.postJson("logs*/_search", query6); + + Assert.assertEquals(HttpStatus.SC_OK, response10.getStatusCode()); + Assert.assertTrue(response10.getBody(), response10.getBody().contains("\"termX\":\"A\"")); + Assert.assertFalse(response10.getBody(), response10.getBody().contains("\"termX\":\"B\"")); + Assert.assertFalse(response10.getBody(), response10.getBody().contains("\"termX\":\"C\"")); + Assert.assertFalse(response10.getBody(), response10.getBody().contains("\"termX\":\"D\"")); + Assert.assertFalse(response10.getBody(), response10.getBody().contains("\"termX\":\"E\"")); + + // Admin with setting "min_doc_count":0. Expected to have access to all buckets". + HttpResponse response11 = adminClient.postJson("logs*/_search", query5); + + Assert.assertEquals(HttpStatus.SC_OK, response11.getStatusCode()); + Assert.assertTrue(response11.getBody(), response11.getBody().contains("\"termX\":\"A\"")); + Assert.assertTrue(response11.getBody(), response11.getBody().contains("\"termX\":\"B\"")); + Assert.assertTrue(response11.getBody(), response11.getBody().contains("\"termX\":\"C\"")); + Assert.assertTrue(response11.getBody(), response11.getBody().contains("\"termX\":\"D\"")); + Assert.assertTrue(response11.getBody(), response11.getBody().contains("\"termX\":\"E\"")); + + // Admin without setting "min_doc_count". Expected to have access to all buckets". + HttpResponse response12 = adminClient.postJson("logs*/_search", query6); + + Assert.assertEquals(HttpStatus.SC_OK, response12.getStatusCode()); + Assert.assertTrue(response12.getBody(), response12.getBody().contains("\"termX\":\"A\"")); + Assert.assertTrue(response12.getBody(), response12.getBody().contains("\"termX\":\"B\"")); + Assert.assertTrue(response12.getBody(), response12.getBody().contains("\"termX\":\"C\"")); + Assert.assertTrue(response12.getBody(), response12.getBody().contains("\"termX\":\"D\"")); + Assert.assertTrue(response12.getBody(), response12.getBody().contains("\"termX\":\"E\"")); + + // Date Histogram Aggregation is not impacted. + // Non-admin user with setting "min_doc_count=0". Expected to only have access to buckets for dept_manager". + String query7 = "{\"aggs\":{\"timestamp\":{\"date_histogram\":{\"field\":\"timestamp\",\"calendar_interval\":\"month\",\"min_doc_count\":0}}}}"; + + HttpResponse response13 = dmClient.postJson("logs*/_search", query7); + + Assert.assertEquals(HttpStatus.SC_OK, response13.getStatusCode()); + Assert.assertTrue(response13.getBody(), response13.getBody().contains("\"termX\":\"A\"")); + Assert.assertFalse(response13.getBody(), response13.getBody().contains("\"termX\":\"B\"")); + Assert.assertFalse(response13.getBody(), response13.getBody().contains("\"termX\":\"C\"")); + Assert.assertFalse(response13.getBody(), response13.getBody().contains("\"termX\":\"D\"")); + Assert.assertFalse(response13.getBody(), response13.getBody().contains("\"termX\":\"E\"")); + + // Non-admin user without setting "min_doc_count". Expected to only have access to buckets for dept_manager". + String query8 = "{\"aggs\":{\"timestamp\":{\"date_histogram\":{\"field\":\"timestamp\",\"calendar_interval\":\"month\"}}}}"; + + HttpResponse response14 = dmClient.postJson("logs*/_search", query8); + + Assert.assertEquals(HttpStatus.SC_OK, response14.getStatusCode()); + Assert.assertTrue(response14.getBody(), response14.getBody().contains("\"termX\":\"A\"")); + Assert.assertFalse(response14.getBody(), response14.getBody().contains("\"termX\":\"B\"")); + Assert.assertFalse(response14.getBody(), response14.getBody().contains("\"termX\":\"C\"")); + Assert.assertFalse(response14.getBody(), response14.getBody().contains("\"termX\":\"D\"")); + Assert.assertFalse(response14.getBody(), response14.getBody().contains("\"termX\":\"E\"")); + + // Admin with setting "min_doc_count":0. Expected to have access to all buckets". + HttpResponse response15 = adminClient.postJson("logs*/_search", query7); + + Assert.assertEquals(HttpStatus.SC_OK, response15.getStatusCode()); + Assert.assertTrue(response15.getBody(), response15.getBody().contains("\"termX\":\"A\"")); + Assert.assertTrue(response15.getBody(), response15.getBody().contains("\"termX\":\"B\"")); + Assert.assertTrue(response15.getBody(), response15.getBody().contains("\"termX\":\"C\"")); + Assert.assertTrue(response15.getBody(), response15.getBody().contains("\"termX\":\"D\"")); + Assert.assertTrue(response15.getBody(), response15.getBody().contains("\"termX\":\"E\"")); + + // Admin without setting "min_doc_count". Expected to have access to all buckets". + HttpResponse response16 = adminClient.postJson("logs*/_search", query8); + + Assert.assertEquals(HttpStatus.SC_OK, response16.getStatusCode()); + Assert.assertTrue(response16.getBody(), response16.getBody().contains("\"termX\":\"A\"")); + Assert.assertTrue(response16.getBody(), response16.getBody().contains("\"termX\":\"B\"")); + Assert.assertTrue(response16.getBody(), response16.getBody().contains("\"termX\":\"C\"")); + Assert.assertTrue(response16.getBody(), response16.getBody().contains("\"termX\":\"D\"")); + Assert.assertTrue(response16.getBody(), response16.getBody().contains("\"termX\":\"E\"")); + } + } + } \ No newline at end of file diff --git a/dlic-dlsfls/src/test/resources/dlsfls/sg_roles.yml b/dlic-dlsfls/src/test/resources/dlsfls/sg_roles.yml index dbd9fe42e..bb1fa7c65 100644 --- a/dlic-dlsfls/src/test/resources/dlsfls/sg_roles.yml +++ b/dlic-dlsfls/src/test/resources/dlsfls/sg_roles.yml @@ -2553,3 +2553,11 @@ sg_combined: allowed_actions: - "READ" tenant_permissions: [] +logs_index_with_dls: + index_permissions: + - index_patterns: + - "logs" + dls: "{\"bool\": {\"filter\": {\"term\" : {\"termX\" : \"A\" }}}}" + masked_fields: null + allowed_actions: + - "SGS_READ" diff --git a/dlic-dlsfls/src/test/resources/dlsfls/sg_roles_mapping.yml b/dlic-dlsfls/src/test/resources/dlsfls/sg_roles_mapping.yml index eda1823be..3bd873f27 100644 --- a/dlic-dlsfls/src/test/resources/dlsfls/sg_roles_mapping.yml +++ b/dlic-dlsfls/src/test/resources/dlsfls/sg_roles_mapping.yml @@ -255,4 +255,9 @@ sg_dfm_nv_not_restricted_all_indices: sg_dfm_nv_not_restricted_one_index: users: - dfm_restricted_and_unrestricted_one_index_role + + +logs_index_with_dls: + users: + - dept_manager \ No newline at end of file