diff --git a/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java b/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java index f32051967c..ab4abb6f55 100644 --- a/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java +++ b/bwc-test/src/test/java/org/opensearch/security/bwc/SecurityBackwardsCompatibilityIT.java @@ -337,7 +337,7 @@ private void ingestData(String index) throws IOException { bulkRequestBody.append(Song.randomSong().asJson() + "\n"); } List responses = RestHelper.requestAgainstAllNodes( - testUserRestClient, + adminClient(), "POST", "_bulk?refresh=wait_for", new StringEntity(bulkRequestBody.toString(), APPLICATION_NDJSON) @@ -413,30 +413,31 @@ private boolean resourceExists(String url) throws IOException { */ private void createTestRoleIfNotExists(String role) throws IOException { String url = "_plugins/_security/api/roles/" + role; - String roleSettings = "{\n" - + " \"cluster_permissions\": [\n" - + " \"unlimited\"\n" - + " ],\n" - + " \"index_permissions\": [\n" - + " {\n" - + " \"index_patterns\": [\n" - + " \"test_index*\"\n" - + " ],\n" - + " \"dls\": \"{ \\\"bool\\\": { \\\"must\\\": { \\\"match\\\": { \\\"genre\\\": \\\"rock\\\" } } } }\",\n" - + " \"fls\": [\n" - + " \"~lyrics\"\n" - + " ],\n" - + " \"masked_fields\": [\n" - + " \"artist\"\n" - + " ],\n" - + " \"allowed_actions\": [\n" - + " \"read\",\n" - + " \"write\"\n" - + " ]\n" - + " }\n" - + " ],\n" - + " \"tenant_permissions\": []\n" - + "}\n"; + String roleSettings = """ + { + "cluster_permissions": [ + "unlimited" + ], + "index_permissions": [ + { + "index_patterns": [ + "test_index*" + ], + "dls": "{ \\\"bool\\\": { \\\"must\\\": { \\\"match\\\": { \\\"genre\\\": \\\"rock\\\" } } } }", + "fls": [ + "~lyrics" + ], + "masked_fields": [ + "artist" + ], + "allowed_actions": [ + "read" + ] + } + ], + "tenant_permissions": [] + } + """; Response response = RestHelper.makeRequest(adminClient(), "PUT", url, RestHelper.toHttpEntity(roleSettings)); assertThat(response.getStatusLine().getStatusCode(), anyOf(equalTo(200), equalTo(201))); diff --git a/src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java new file mode 100644 index 0000000000..c6468d6280 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/dlsfls/DlsWriteBlockedIntegrationTest.java @@ -0,0 +1,140 @@ +/* + * 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.dlsfls; + +import java.io.IOException; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.security.support.ConfigConstants; +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 static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + +/** + * Integration tests for DLS_WRITE_BLOCKED setting which blocks write operations + * when users have DLS, FLS, or Field Masking restrictions. + */ +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class DlsWriteBlockedIntegrationTest { + + private static final String DLS_INDEX = "dls_index"; + private static final String FLS_INDEX = "fls_index"; + private static final String NO_RESTRICTION_INDEX = "no_restriction_index"; + + static final User ADMIN_USER = new User("admin").roles(ALL_ACCESS); + + static final User DLS_USER = new User("dls_user").roles( + new Role("dls_role").clusterPermissions("*").indexPermissions("*").dls("{\"term\": {\"dept\": \"sales\"}}").on(DLS_INDEX) + ); + + static final User FLS_USER = new User("fls_user").roles( + new Role("fls_role").clusterPermissions("*").indexPermissions("*").fls("public").on(FLS_INDEX) + ); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER, DLS_USER, FLS_USER) + .build(); + + @BeforeClass + public static void createTestData() throws IOException { + try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + client.putJson(DLS_INDEX + "/_doc/1?refresh=true", "{\"dept\":\"sales\",\"amount\":100}"); + client.putJson(FLS_INDEX + "/_doc/1?refresh=true", "{\"public\":\"data\",\"secret\":\"hidden\"}"); + client.putJson(NO_RESTRICTION_INDEX + "/_doc/1?refresh=true", "{\"data\":\"value1\"}"); + } + } + + private void setDlsWriteBlocked(boolean enabled) throws IOException { + try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + client.putJson( + "_cluster/settings", + String.format("{\"transient\":{\"%s\":%b}}", ConfigConstants.SECURITY_DLS_WRITE_BLOCKED, enabled) + ); + } + } + + @Test + public void testDlsUser_CanWrite_WhenSettingDisabled() throws IOException { + setDlsWriteBlocked(false); + try (TestRestClient client = cluster.getRestClient(DLS_USER)) { + var response = client.putJson(DLS_INDEX + "/_doc/test1?refresh=true", "{\"dept\":\"sales\",\"amount\":400}"); + + assertThat(response.getStatusCode(), is(201)); + } + } + + @Test + public void testDlsUser_CannotWrite_WhenSettingEnabled() throws IOException { + setDlsWriteBlocked(true); + try (TestRestClient client = cluster.getRestClient(DLS_USER)) { + var response = client.putJson(DLS_INDEX + "/_doc/test2?refresh=true", "{\"dept\":\"sales\",\"amount\":400}"); + + assertThat(response.getStatusCode(), is(500)); + assertThat(response.getBody(), containsString("is not supported when FLS or DLS or Fieldmasking is activated")); + } + } + + @Test + public void testFlsUser_CanWrite_WhenSettingDisabled() throws IOException { + setDlsWriteBlocked(false); + try (TestRestClient client = cluster.getRestClient(FLS_USER)) { + var response = client.putJson(FLS_INDEX + "/_doc/test3?refresh=true", "{\"public\":\"new_data\",\"secret\":\"new_secret\"}"); + + assertThat(response.getStatusCode(), is(201)); + } + } + + @Test + public void testFlsUser_CannotWrite_WhenSettingEnabled() throws IOException { + setDlsWriteBlocked(true); + try (TestRestClient client = cluster.getRestClient(FLS_USER)) { + var response = client.putJson(FLS_INDEX + "/_doc/test4?refresh=true", "{\"public\":\"new_data\",\"secret\":\"new_secret\"}"); + + assertThat(response.getStatusCode(), is(500)); + assertThat(response.getBody(), containsString("is not supported when FLS or DLS or Fieldmasking is activated")); + } + } + + @Test + public void testAdminUser_CanWrite_WhenSettingEnabled() throws IOException { + setDlsWriteBlocked(true); + try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + var response = client.putJson(DLS_INDEX + "/_doc/test6?refresh=true", "{\"dept\":\"admin\",\"amount\":999}"); + + assertThat(response.getStatusCode(), is(201)); + } + } + + @Test + public void testDlsUser_CanRead_WhenSettingEnabled() throws IOException { + setDlsWriteBlocked(true); + try (TestRestClient client = cluster.getRestClient(DLS_USER)) { + var response = client.get(DLS_INDEX + "/_search"); + + assertThat(response.getStatusCode(), is(200)); + } + } +} diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 57af519311..8ed0d6de9b 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -2269,6 +2269,7 @@ public List> getSettings() { ); settings.add(SecuritySettings.USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING); + settings.add(SecuritySettings.DLS_WRITE_BLOCKED); } return settings; diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 8f9b1cc6c6..122ef92c86 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -85,11 +85,14 @@ import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.HeaderHelper; +import org.opensearch.security.support.SecuritySettings; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.client.Client; import static org.opensearch.security.privileges.PrivilegesEvaluatorImpl.isClusterPerm; +import static org.opensearch.security.support.ConfigConstants.SECURITY_DLS_WRITE_BLOCKED; +import static org.opensearch.security.support.ConfigConstants.SECURITY_DLS_WRITE_BLOCKED_ENABLED_DEFAULT; public class DlsFlsValveImpl implements DlsFlsRequestValve { @@ -109,6 +112,7 @@ public class DlsFlsValveImpl implements DlsFlsRequestValve { private final AdminDNs adminDNs; private final OpensearchDynamicSetting resourceSharingEnabledSetting; private final ResourcePluginInfo resourcePluginInfo; + private volatile boolean dlsWriteBlockedEnabled; public DlsFlsValveImpl( Settings settings, @@ -142,6 +146,12 @@ public DlsFlsValveImpl( config.updateClusterStateMetadataAsync(clusterService, threadPool); } }); + this.dlsWriteBlockedEnabled = settings.getAsBoolean(SECURITY_DLS_WRITE_BLOCKED, SECURITY_DLS_WRITE_BLOCKED_ENABLED_DEFAULT); + if (clusterService.getClusterSettings() != null) { + clusterService.getClusterSettings().addSettingsUpdateConsumer(SecuritySettings.DLS_WRITE_BLOCKED, newDlsWriteBlockedEnabled -> { + dlsWriteBlockedEnabled = newDlsWriteBlockedEnabled; + }); + } this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; } @@ -333,11 +343,21 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< if (request instanceof BulkShardRequest) { for (BulkItemRequest inner : ((BulkShardRequest) request).items()) { - if (inner.request() instanceof UpdateRequest) { + if (dlsWriteBlockedEnabled) { listener.onFailure( - new OpenSearchSecurityException("Update is not supported when FLS or DLS or Fieldmasking is activated") + new OpenSearchSecurityException( + inner.request().getClass().getSimpleName() + + " is not supported when FLS or DLS or Fieldmasking is activated" + ) ); return false; + } else { + if (inner.request() instanceof UpdateRequest) { + listener.onFailure( + new OpenSearchSecurityException("Update is not supported when FLS or DLS or Fieldmasking is activated") + ); + return false; + } } } } diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index a7a271c758..37eb41a771 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -340,6 +340,8 @@ public enum RolesMappingResolution { public static final String SECURITY_FILTER_SECURITYINDEX_FROM_ALL_REQUESTS = SECURITY_SETTINGS_PREFIX + "filter_securityindex_from_all_requests"; public static final String SECURITY_DLS_MODE = SECURITY_SETTINGS_PREFIX + "dls.mode"; + public static final String SECURITY_DLS_WRITE_BLOCKED = SECURITY_SETTINGS_PREFIX + "dls.write_blocked"; + public static final boolean SECURITY_DLS_WRITE_BLOCKED_ENABLED_DEFAULT = false; // REST API public static final String SECURITY_RESTAPI_ROLES_ENABLED = SECURITY_SETTINGS_PREFIX + "restapi.roles_enabled"; public static final String SECURITY_RESTAPI_ADMIN_ENABLED = SECURITY_SETTINGS_PREFIX + "restapi.admin.enabled"; diff --git a/src/main/java/org/opensearch/security/support/SecuritySettings.java b/src/main/java/org/opensearch/security/support/SecuritySettings.java index 4a442c9316..cb5e6c1cd1 100644 --- a/src/main/java/org/opensearch/security/support/SecuritySettings.java +++ b/src/main/java/org/opensearch/security/support/SecuritySettings.java @@ -42,4 +42,11 @@ public class SecuritySettings { Setting.Property.NodeScope, Setting.Property.Dynamic ); // Not filtered + + public static final Setting DLS_WRITE_BLOCKED = Setting.boolSetting( + ConfigConstants.SECURITY_DLS_WRITE_BLOCKED, + false, + Setting.Property.NodeScope, + Setting.Property.Dynamic + ); }