Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ private void ingestData(String index) throws IOException {
bulkRequestBody.append(Song.randomSong().asJson() + "\n");
}
List<Response> responses = RestHelper.requestAgainstAllNodes(
testUserRestClient,
adminClient(),
"POST",
"_bulk?refresh=wait_for",
new StringEntity(bulkRequestBody.toString(), APPLICATION_NDJSON)
Expand Down Expand Up @@ -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)));
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2269,6 +2269,7 @@ public List<Setting<?>> getSettings() {
);

settings.add(SecuritySettings.USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING);
settings.add(SecuritySettings.DLS_WRITE_BLOCKED);
}

return settings;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -109,6 +112,7 @@ public class DlsFlsValveImpl implements DlsFlsRequestValve {
private final AdminDNs adminDNs;
private final OpensearchDynamicSetting<Boolean> resourceSharingEnabledSetting;
private final ResourcePluginInfo resourcePluginInfo;
private volatile boolean dlsWriteBlockedEnabled;

public DlsFlsValveImpl(
Settings settings,
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,11 @@ public class SecuritySettings {
Setting.Property.NodeScope,
Setting.Property.Dynamic
); // Not filtered

public static final Setting<Boolean> DLS_WRITE_BLOCKED = Setting.boolSetting(
ConfigConstants.SECURITY_DLS_WRITE_BLOCKED,
false,
Setting.Property.NodeScope,
Setting.Property.Dynamic
);
}
Loading