diff --git a/CHANGELOG.md b/CHANGELOG.md index 68384915c9..7a8898aaf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Features ### Enhancements +- Makes resource settings dynamic ([#5677](https://github.com/opensearch-project/security/pull/5677)) ### Bug Fixes - Create a WildcardMatcher.NONE when creating a WildcardMatcher with an empty string ([#5694](https://github.com/opensearch-project/security/pull/5694)) diff --git a/RESOURCE_SHARING_AND_ACCESS_CONTROL.md b/RESOURCE_SHARING_AND_ACCESS_CONTROL.md index 0eb99b14d2..e03fd2b21d 100644 --- a/RESOURCE_SHARING_AND_ACCESS_CONTROL.md +++ b/RESOURCE_SHARING_AND_ACCESS_CONTROL.md @@ -499,7 +499,7 @@ Since no entities are listed, the resource is accessible **only by its creator a ### **Additional Notes** - **Feature Flag:** These APIs are available only when `plugins.security.experimental.resource_sharing.enabled` is set to `true` in the configuration. - +- **Protected Types:** These APIs will only come into effect if concerned resources are marked as protected: `plugins.security.experimental.resource_sharing.protected_types: [, ]`. --- @@ -529,6 +529,58 @@ The list of protected types are controlled through following opensearch setting ``` NOTE: These types will be available on documentation website. +### **Dynamic Updates** + +The settings described above can be dynamically updated at runtime using the OpenSearch `_cluster/settings` API. +This allows administrators to enable or disable the **Resource Sharing** feature and modify the list of **protected types** without restarting the cluster. + +#### **Example 1: Enable Resource Sharing Feature** + +```bash +PUT _cluster/settings +{ + "transient": { + "plugins.security.experimental.resource_sharing.enabled": true + } +} +``` + +#### **Example 2: Update Protected Types** + +```bash +PUT _cluster/settings +{ + "transient": { + "plugins.security.experimental.resource_sharing.protected_types": ["sample-resource", "ml-model"] + } +} +``` + +#### **Example 3: Clear Protected Types** + +```bash +PUT _cluster/settings +{ + "transient": { + "plugins.security.experimental.resource_sharing.protected_types": [] + } +} +``` + +#### **Notes** + +* Both settings support **dynamic updates**, meaning the changes take effect immediately without requiring a node restart. +* You can use either **transient** (temporary until restart) or **persistent** (survive restarts) settings. +* To verify the current values, use: + + ```bash + GET _cluster/settings?include_defaults=true + ``` +* Feature toggles and protected type lists can also be modified through configuration files before cluster startup if preferred. + + + + ## **2. User Setup** To enable users to interact with the **Resource Sharing and Access Control** feature, they must be assigned the appropriate cluster permissions along with resource-specific access. diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/SecurityDisabledTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/SecurityDisabledTests.java index fbd460efcc..fe651253ca 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/SecurityDisabledTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/SecurityDisabledTests.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Map; +import java.util.Set; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.http.HttpStatus; @@ -22,6 +23,8 @@ import org.opensearch.plugins.PluginInfo; import org.opensearch.sample.SampleResourcePlugin; import org.opensearch.security.OpenSearchSecurityPlugin; +import org.opensearch.security.spi.resources.sharing.Recipient; +import org.opensearch.security.spi.resources.sharing.Recipients; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -33,12 +36,11 @@ import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_CREATE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_DELETE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_GET_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_REVOKE_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_UPDATE_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.revokeAccessPayload; -import static org.opensearch.sample.resource.TestUtils.shareWithPayload; +import static org.opensearch.sample.resource.TestUtils.SECURITY_SHARE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.putSharingInfoPayload; import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** @@ -109,17 +111,18 @@ public void testSamplePluginAPIs() { response = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + resourceId, sampleResourceUpdated); response.assertStatusCode(HttpStatus.SC_OK); - response = client.postJson( - SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, - shareWithPayload(USER_ADMIN.getName(), SAMPLE_READ_ONLY_RESOURCE_AG) + response = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload(resourceId, RESOURCE_TYPE, SAMPLE_READ_ONLY_RESOURCE_AG, Recipient.USERS, USER_ADMIN.getName()) ); - assertNotImplementedResponse(response, "Cannot share resource"); + assertBadRequest(response, "no handler found for uri [/_plugins/_security/api/resource/share] and method [PUT]"); - response = client.postJson( - SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + resourceId, - revokeAccessPayload(USER_ADMIN.getName(), SAMPLE_READ_ONLY_RESOURCE_AG) - ); - assertNotImplementedResponse(response, "Cannot revoke access to resource"); + TestUtils.PatchSharingInfoPayloadBuilder patchBuilder = new TestUtils.PatchSharingInfoPayloadBuilder(); + patchBuilder.resourceType(RESOURCE_TYPE); + patchBuilder.resourceId(resourceId); + patchBuilder.revoke(new Recipients(Map.of(Recipient.USERS, Set.of(USER_ADMIN.getName()))), SAMPLE_READ_ONLY_RESOURCE_AG); + response = client.patch(SECURITY_SHARE_ENDPOINT, patchBuilder.build()); + assertBadRequest(response, "no handler found for uri [/_plugins/_security/api/resource/share] and method [PATCH]"); response = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + resourceId); response.assertStatusCode(HttpStatus.SC_OK); @@ -127,7 +130,7 @@ public void testSamplePluginAPIs() { } } - private void assertNotImplementedResponse(TestRestClient.HttpResponse response, String msg) { - assertThat(response, RestMatchers.isMethodNotImplemented("/error/reason", msg)); + private void assertBadRequest(TestRestClient.HttpResponse response, String msg) { + assertThat(response, RestMatchers.isBadRequest("/error", msg)); } } diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java index 6bd3b40bbc..919deec80e 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/TestUtils.java @@ -14,6 +14,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import org.apache.hc.core5.http.Header; import org.apache.http.HttpStatus; @@ -26,6 +27,7 @@ import org.opensearch.plugins.PluginInfo; import org.opensearch.sample.SampleResourcePlugin; import org.opensearch.security.OpenSearchSecurityPlugin; +import org.opensearch.security.spi.resources.sharing.Recipient; import org.opensearch.security.spi.resources.sharing.Recipients; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.certificate.CertificateData; @@ -66,8 +68,8 @@ public final class TestUtils { "cluster:admin/sample-resource-plugin/get", "cluster:admin/sample-resource-plugin/search", "cluster:admin/sample-resource-plugin/create", - "cluster:admin/sample-resource-plugin/share", - "cluster:admin/sample-resource-plugin/revoke" + "cluster:admin/security/resource/share", + "cluster:admin/security/resource/share" ).indexPermissions("indices:data/read*").on(RESOURCE_INDEX_NAME) ); @@ -83,8 +85,6 @@ public final class TestUtils { public static final String SAMPLE_RESOURCE_UPDATE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/update"; public static final String SAMPLE_RESOURCE_DELETE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/delete"; public static final String SAMPLE_RESOURCE_SEARCH_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/search"; - public static final String SAMPLE_RESOURCE_SHARE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/share"; - public static final String SAMPLE_RESOURCE_REVOKE_ENDPOINT = SAMPLE_RESOURCE_PLUGIN_PREFIX + "/revoke"; public static final String RESOURCE_SHARING_MIGRATION_ENDPOINT = "_plugins/_security/api/resources/migrate"; public static final String SECURITY_SHARE_ENDPOINT = "_plugins/_security/api/resource/share"; @@ -126,18 +126,6 @@ public static LocalCluster newCluster(boolean featureEnabled, boolean systemInde .build(); } - public static String shareWithPayload(String user, String accessLevel) { - return """ - { - "share_with": { - "%s" : { - "users": ["%s"] - } - } - } - """.formatted(accessLevel, user); - } - public static String directSharePayload(String resourceId, String creator, String target, String accessLevel) { return """ { @@ -154,31 +142,6 @@ public static String directSharePayload(String resourceId, String creator, Strin """.formatted(resourceId, creator, accessLevel, target); } - public static String revokeAccessPayload(String user, String accessLevel) { - return """ - { - "entities_to_revoke": { - "%s" : { - "users": ["%s"] - } - } - } - """.formatted(accessLevel, user); - - } - - public static String shareWithRolePayload(String role, String accessLevel) { - return """ - { - "share_with": { - "%s" : { - "roles": ["%s"] - } - } - } - """.formatted(accessLevel, role); - } - public static String migrationPayload_valid() { return """ { @@ -227,18 +190,24 @@ public static String migrationPayload_missingBackendRoles() { """.formatted(RESOURCE_INDEX_NAME, "user/name"); } - public static String putSharingInfoPayload(String resourceId, String resourceType, String accessLevel, String user) { + public static String putSharingInfoPayload( + String resourceId, + String resourceType, + String accessLevel, + Recipient recipient, + String entity + ) { return """ { "resource_id": "%s", "resource_type": "%s", "share_with": { "%s" : { - "users": ["%s"] + "%s": ["%s"] } } } - """.formatted(resourceId, resourceType, accessLevel, user); + """.formatted(resourceId, resourceType, accessLevel, recipient.getName(), entity); } public static class PatchSharingInfoPayloadBuilder { @@ -335,7 +304,6 @@ public void wipeOutResourceEntries() { String jsonBody = "{ \"query\": { \"match_all\": {} } }"; TestRestClient.HttpResponse resp = client.postJson(endpoint, jsonBody); resp.assertStatusCode(HttpStatus.SC_OK); - } } @@ -522,7 +490,7 @@ private void assertUpdate(String endpoint, String newName, TestSecurityConfig.Us } } - public void assertDirectShare( + public void assertDirectUpdateSharingInfo( String resourceId, TestSecurityConfig.User user, TestSecurityConfig.User target, @@ -544,43 +512,16 @@ public void assertApiShare( TestSecurityConfig.User target, String accessLevel, int status - ) { - assertShare(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, user, target, accessLevel, status); - } - - private void assertShare( - String endpoint, - TestSecurityConfig.User user, - TestSecurityConfig.User target, - String accessLevel, - int status ) { try (TestRestClient client = cluster.getRestClient(user)) { - TestRestClient.HttpResponse response = client.postJson(endpoint, shareWithPayload(target.getName(), accessLevel)); + TestRestClient.HttpResponse response = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload(resourceId, RESOURCE_TYPE, accessLevel, Recipient.USERS, target.getName()) + ); response.assertStatusCode(status); } } - public void assertDirectRevoke( - String resourceId, - TestSecurityConfig.User user, - TestSecurityConfig.User target, - String accessLevel, - int status - ) { - assertRevoke(RESOURCE_SHARING_INDEX + "/_doc/" + resourceId, user, target, accessLevel, status); - } - - public void assertApiRevoke( - String resourceId, - TestSecurityConfig.User user, - TestSecurityConfig.User target, - String accessLevel, - int status - ) { - assertRevoke(SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + resourceId, user, target, accessLevel, status); - } - public void assertApiShareByRole( String resourceId, TestSecurityConfig.User user, @@ -589,23 +530,27 @@ public void assertApiShareByRole( int status ) { try (TestRestClient client = cluster.getRestClient(user)) { - TestRestClient.HttpResponse response = client.postJson( - SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, - shareWithRolePayload(targetRole, accessLevel) + TestRestClient.HttpResponse response = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload(resourceId, RESOURCE_TYPE, accessLevel, Recipient.ROLES, targetRole) ); response.assertStatusCode(status); } } - private void assertRevoke( - String endpoint, + public void assertApiRevoke( + String resourceId, TestSecurityConfig.User user, TestSecurityConfig.User target, String accessLevel, int status ) { + PatchSharingInfoPayloadBuilder patchBuilder = new PatchSharingInfoPayloadBuilder(); + patchBuilder.resourceType(RESOURCE_TYPE); + patchBuilder.resourceId(resourceId); + patchBuilder.revoke(new Recipients(Map.of(Recipient.USERS, Set.of(target.getName()))), accessLevel); try (TestRestClient client = cluster.getRestClient(user)) { - TestRestClient.HttpResponse response = client.postJson(endpoint, revokeAccessPayload(target.getName(), accessLevel)); + TestRestClient.HttpResponse response = client.patch(TestUtils.SECURITY_SHARE_ENDPOINT, patchBuilder.build()); response.assertStatusCode(status); } } diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/FeatureFlagSettingTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/FeatureFlagSettingTests.java new file mode 100644 index 0000000000..a355cb7742 --- /dev/null +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/FeatureFlagSettingTests.java @@ -0,0 +1,437 @@ +/* + * 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.sample.resource.feature; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import com.carrotsearch.randomizedtesting.RandomizedRunner; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.HttpStatus; +import org.awaitility.Awaitility; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.sample.resource.TestUtils; +import org.opensearch.security.spi.resources.sharing.Recipient; +import org.opensearch.security.spi.resources.sharing.Recipients; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.test.framework.TestSecurityConfig; +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.equalTo; +import static org.opensearch.sample.resource.TestUtils.ApiHelper.searchAllPayload; +import static org.opensearch.sample.resource.TestUtils.ApiHelper.searchByNamePayload; +import static org.opensearch.sample.resource.TestUtils.FULL_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.LIMITED_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.NO_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.RESOURCE_SHARING_MIGRATION_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_FULL_ACCESS_RESOURCE_AG; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_READ_ONLY_RESOURCE_AG; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_CREATE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_DELETE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_GET_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SEARCH_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_UPDATE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SECURITY_SHARE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.migrationPayload_valid; +import static org.opensearch.sample.resource.TestUtils.newCluster; +import static org.opensearch.sample.resource.TestUtils.putSharingInfoPayload; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; +import static org.awaitility.Awaitility.await; + +/** + * Verifies dynamic behavior of cluster setting: + * {@link ConfigConstants#OPENSEARCH_RESOURCE_SHARING_ENABLED} + * + * Phase 1: feature disabled + * Phase 2: flip setting via _cluster/settings and verify enabled behavior + */ +@RunWith(RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class FeatureFlagSettingTests { + + @ClassRule + public static LocalCluster cluster = newCluster(false, true); + + private final TestUtils.ApiHelper api = new TestUtils.ApiHelper(cluster); + private String adminResId; + + // --------- Lifecycle --------- + + @Before + public void setup() { + // Starting with disabled for deterministic ordering + setResourceSharingEnabled(false); + awaitResourceSharingEnabled(false); + + adminResId = createSampleResourceAs(USER_ADMIN); + } + + @After + public void cleanup() { + api.wipeOutResourceEntries(); + // Flip the dynamic cluster setting to false + setResourceSharingEnabled(false); + awaitResourceSharingEnabled(false); + } + + private String createSampleResourceAs(TestSecurityConfig.User user) { + try (TestRestClient client = cluster.getRestClient(user)) { + String sampleResource = """ + { + "name":"sample", + "store_user": true + } + """; + + TestRestClient.HttpResponse response = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sampleResource); + + String resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); + + Awaitility.await() + .alias("Wait until resource data is populated") + .until(() -> client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId).getStatusCode(), equalTo(200)); + return resourceId; + } + } + + // --------- Phase 1: Disabled behavior --------- + + @Test + public void testBehaviorWhenDisabled() { + // “Disabled” expectations: + // - Share api handler not exist -> 501 on endpoints that would be implemented by the feature + // - Access relies purely on existing index/cluster perms ("legacy" RBAC behavior) + + assertNoAccessUser_Disabled(); + assertLimitedUser_Disabled(); + assertFullUser_Disabled(); + assertAdminCert_Disabled(); + } + + // --------- Assertions: Disabled behavior --------- + + private void assertNoAccessUser_Disabled() { + // cannot create + try (TestRestClient c = cluster.getRestClient(NO_ACCESS_USER)) { + TestRestClient.HttpResponse r = c.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, "{\"name\":\"sampleUser\"}"); + r.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + // cannot get, update, delete + api.assertApiGet(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiUpdate(adminResId, NO_ACCESS_USER, "x", HttpStatus.SC_FORBIDDEN); + api.assertApiDelete(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + // share/revoke endpoints exist? -> when disabled we expect 403 (no perm) or 501 (no handler). + api.assertApiShare(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_NOT_IMPLEMENTED); + api.assertApiRevoke(adminResId, NO_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_NOT_IMPLEMENTED); + + // search forbidden + api.assertApiGetSearchForbidden(NO_ACCESS_USER); + api.assertApiPostSearchForbidden(searchAllPayload(), NO_ACCESS_USER); + api.assertApiPostSearchForbidden(searchByNamePayload("sample"), NO_ACCESS_USER); + } + + private void assertLimitedUser_Disabled() { + // create own + String userResId = createSampleResourceAs(LIMITED_ACCESS_USER); + + // see admin resource (disabled path follows index perms) per your “disabled” expectations + api.assertApiGet(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sample"); + api.assertApiGetAll(LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sample"); + + // cannot update (no perm); cannot delete + api.assertApiUpdate(adminResId, LIMITED_ACCESS_USER, "x", HttpStatus.SC_FORBIDDEN); + api.assertApiUpdate(userResId, LIMITED_ACCESS_USER, "x", HttpStatus.SC_FORBIDDEN); + api.assertApiDelete(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + api.assertApiDelete(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + + // share/revoke should be NOT IMPLEMENTED when disabled (your “last 4 tests”) + api.assertApiShare( + adminResId, + LIMITED_ACCESS_USER, + LIMITED_ACCESS_USER, + SAMPLE_READ_ONLY_RESOURCE_AG, + HttpStatus.SC_NOT_IMPLEMENTED + ); + api.assertApiRevoke(adminResId, LIMITED_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_NOT_IMPLEMENTED); + + // can search both resources + api.assertApiGetSearch(LIMITED_ACCESS_USER, HttpStatus.SC_OK, 2, "sample"); + api.assertApiPostSearch(searchAllPayload(), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 2, "sample"); + api.assertApiPostSearch(searchByNamePayload("sample"), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 2, "sample"); + } + + private void assertFullUser_Disabled() { + String userResId = createSampleResourceAs(FULL_ACCESS_USER); + + // full * perms when disabled -> can see & update both + api.assertApiGet(adminResId, FULL_ACCESS_USER, HttpStatus.SC_OK, "sample"); + api.assertApiGetAll(FULL_ACCESS_USER, HttpStatus.SC_OK, "sample"); + api.assertApiUpdate(adminResId, FULL_ACCESS_USER, "sampleUpdateAdmin", HttpStatus.SC_OK); + api.assertApiUpdate(userResId, FULL_ACCESS_USER, "sampleUpdateUser", HttpStatus.SC_OK); + + // share/revoke not implemented + api.assertApiShare(adminResId, FULL_ACCESS_USER, FULL_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_NOT_IMPLEMENTED); + api.assertApiRevoke(adminResId, FULL_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_NOT_IMPLEMENTED); + + // search sees both + api.assertApiGetSearch(FULL_ACCESS_USER, HttpStatus.SC_OK, 3, "sampleUpdateAdmin"); // admin + full user + limited user created + // above + api.assertApiPostSearch(searchAllPayload(), FULL_ACCESS_USER, HttpStatus.SC_OK, 3, "sampleUpdateAdmin"); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateAdmin"), FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateAdmin"); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateUser"), FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // can delete both + api.assertApiDelete(userResId, FULL_ACCESS_USER, HttpStatus.SC_OK); + api.assertApiDelete(adminResId, FULL_ACCESS_USER, HttpStatus.SC_OK); + } + + private void assertAdminCert_Disabled() { + adminResId = createSampleResourceAs(USER_ADMIN); + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // read / update ok + TestRestClient.HttpResponse resp = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + adminResId); + resp.assertStatusCode(HttpStatus.SC_OK); + + resp = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + adminResId, "{\"name\":\"sampleUpdated\"}"); + resp.assertStatusCode(HttpStatus.SC_OK); + + // share/revoke not implemented + resp = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + Recipient.USERS, + FULL_ACCESS_USER.getName() + ) + ); + resp.assertStatusCode(HttpStatus.SC_NOT_IMPLEMENTED); + + TestUtils.PatchSharingInfoPayloadBuilder payloadBuilder = new TestUtils.PatchSharingInfoPayloadBuilder(); + payloadBuilder.resourceId(adminResId); + payloadBuilder.resourceType(RESOURCE_TYPE); + payloadBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(FULL_ACCESS_USER.getName()))), + SAMPLE_FULL_ACCESS_RESOURCE_AG + ); + resp = client.patch(SECURITY_SHARE_ENDPOINT, payloadBuilder.build()); + resp.assertStatusCode(HttpStatus.SC_NOT_IMPLEMENTED); + + // search works + resp = client.get(SAMPLE_RESOURCE_SEARCH_ENDPOINT); + resp.assertStatusCode(HttpStatus.SC_OK); + + // delete ok + resp = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + adminResId); + resp.assertStatusCode(HttpStatus.SC_OK); + } + } + + // --------- Phase 2: Flip to Enabled then verify enabled behavior --------- + + @Test + public void testBehaviorAfterEnabling() { + // Flip the dynamic cluster setting to true + setResourceSharingEnabled(true); + awaitResourceSharingEnabled(true); + + // migrate existing resources to new records + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + TestRestClient.HttpResponse migrateResponse = client.postJson(RESOURCE_SHARING_MIGRATION_ENDPOINT, migrationPayload_valid()); + migrateResponse.assertStatusCode(HttpStatus.SC_OK); + assertThat(migrateResponse.bodyAsMap().get("summary"), equalTo("Migration complete. migrated 1; skippedNoUser 0; failed 0")); + } + + // “Enabled” expectations: + // - Share/Revoke handlers exist -> return permission-based 200/403 (not 501) + // - Search and read access follow resource-sharing rules + // - Owners can share/revoke; others constrained + + assertNoAccessUser_Enabled(); + assertLimitedUser_Enabled(); + assertFullUser_Enabled(); + assertAdminCert_Enabled(); + } + + // --------- Helpers: cluster setting flips & awaits --------- + private void setResourceSharingEnabled(boolean enabled) { + String body = String.format("{\"transient\":{\"%s\":%s}}", ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, enabled); + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + TestRestClient.HttpResponse resp = client.putJson("_cluster/settings", body); + resp.assertStatusCode(HttpStatus.SC_OK); + } + } + + /** + * Confirm the setting took effect + */ + private void awaitResourceSharingEnabled(boolean expected) { + // Wait for cluster setting to reflect desired value + await().atMost(30, TimeUnit.SECONDS).pollInterval(200, TimeUnit.MILLISECONDS).until(() -> readSettingEquals(expected)); + } + + private boolean readSettingEquals(boolean expected) { + try (var client = cluster.getRestClient(cluster.getAdminCertificate())) { + var r = client.get("_cluster/settings?include_defaults=true&flat_settings=true"); + r.assertStatusCode(200); + String key = "\"" + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED + "\":\"" + expected + "\""; + return r.getBody().contains(key); + } catch (Exception e) { + return false; + } + } + + // --------- Assertions: Enabled behavior --------- + + private void assertNoAccessUser_Enabled() { + // cannot create + try (TestRestClient c = cluster.getRestClient(NO_ACCESS_USER)) { + TestRestClient.HttpResponse r = c.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, "{\"name\":\"sampleUser\"}"); + r.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + // cannot get/update/delete + api.assertApiGet(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiUpdate(adminResId, NO_ACCESS_USER, "x", HttpStatus.SC_FORBIDDEN); + api.assertApiDelete(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + + // share/revoke forbidden (handlers now exist → 403 vs 501) + api.assertApiShare(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertApiRevoke(adminResId, NO_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + + // search forbidden + api.assertApiGetSearchForbidden(NO_ACCESS_USER); + api.assertApiPostSearchForbidden(searchAllPayload(), NO_ACCESS_USER); + api.assertApiPostSearchForbidden(searchByNamePayload("sample"), NO_ACCESS_USER); + } + + private void assertLimitedUser_Enabled() { + String userResId = createSampleResourceAs(LIMITED_ACCESS_USER); + + // cannot see admin resource under sharing rules + api.assertApiGet(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiGetAll(LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sample"); + + // can update own; not others + api.assertApiUpdate(adminResId, LIMITED_ACCESS_USER, "sampleUpdateAdmin", HttpStatus.SC_FORBIDDEN); + api.assertApiUpdate(userResId, LIMITED_ACCESS_USER, "sampleUpdateUser", HttpStatus.SC_OK); + api.assertApiGet(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sampleUpdateUser"); + api.assertApiGetSearch(LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // cannot share/revoke others, can share own + api.assertApiShare(adminResId, LIMITED_ACCESS_USER, LIMITED_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertApiRevoke(adminResId, LIMITED_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + + api.assertApiGet(userResId, USER_ADMIN, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiShare(userResId, LIMITED_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertApiGet(userResId, USER_ADMIN, HttpStatus.SC_OK, "sampleUpdateUser"); + api.assertApiRevoke(userResId, LIMITED_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertApiGet(userResId, USER_ADMIN, HttpStatus.SC_FORBIDDEN, ""); + + // searches aligned with ownership + api.assertApiGetSearch(LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiPostSearch(searchAllPayload(), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiPostSearch(searchByNamePayload("sample"), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 0, ""); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateUser"), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // can delete own + api.assertApiDelete(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK); + // cannot delete admin's + api.assertApiDelete(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + } + + private void assertFullUser_Enabled() { + String userResId = createSampleResourceAs(FULL_ACCESS_USER); + + // even with * perms, sharing rules restrict access to others’ resources + api.assertApiGet(adminResId, FULL_ACCESS_USER, HttpStatus.SC_FORBIDDEN, "sample"); + api.assertApiGetAll(FULL_ACCESS_USER, HttpStatus.SC_OK, "sample"); + + // can update own + api.assertApiUpdate(adminResId, FULL_ACCESS_USER, "sampleUpdateAdmin", HttpStatus.SC_FORBIDDEN); + api.assertApiUpdate(userResId, FULL_ACCESS_USER, "sampleUpdateUser", HttpStatus.SC_OK); + api.assertApiGet(userResId, FULL_ACCESS_USER, HttpStatus.SC_OK, "sampleUpdateUser"); + api.assertApiGetSearch(FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // cannot share/revoke others’ resources; can share own + api.assertApiShare(adminResId, FULL_ACCESS_USER, FULL_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertApiRevoke(adminResId, FULL_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + + api.assertApiGet(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiShare(userResId, FULL_ACCESS_USER, LIMITED_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertApiGet(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sampleUpdateUser"); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateUser"), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiRevoke(userResId, FULL_ACCESS_USER, LIMITED_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertApiGet(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + + // search visibility matches sharing state + api.assertApiGetSearch(FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiPostSearch(searchAllPayload(), FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiPostSearch(searchByNamePayload("sample"), FULL_ACCESS_USER, HttpStatus.SC_OK, 0, ""); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateUser"), FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // can delete own; cannot delete admin’s under sharing rules + api.assertApiDelete(userResId, FULL_ACCESS_USER, HttpStatus.SC_OK); + api.assertApiDelete(adminResId, FULL_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + } + + private void assertAdminCert_Enabled() { + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // read/update + TestRestClient.HttpResponse resp = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + adminResId); + resp.assertStatusCode(HttpStatus.SC_OK); + + resp = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + adminResId, "{\"name\":\"sampleUpdated\"}"); + resp.assertStatusCode(HttpStatus.SC_OK); + assertThat(resp.getBody(), containsString("sampleUpdated")); + + // share/revoke handlers exist → expect 200 for admin path + resp = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_FULL_ACCESS_RESOURCE_AG, Recipient.USERS, NO_ACCESS_USER.getName()) + ); + resp.assertStatusCode(HttpStatus.SC_OK); + + TestUtils.PatchSharingInfoPayloadBuilder payloadBuilder = new TestUtils.PatchSharingInfoPayloadBuilder(); + payloadBuilder.resourceId(adminResId); + payloadBuilder.resourceType(RESOURCE_TYPE); + payloadBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(NO_ACCESS_USER.getName()))), + SAMPLE_FULL_ACCESS_RESOURCE_AG + ); + resp = client.patch(SECURITY_SHARE_ENDPOINT, payloadBuilder.build()); + resp.assertStatusCode(HttpStatus.SC_OK); + + // search works + resp = client.get(SAMPLE_RESOURCE_SEARCH_ENDPOINT); + resp.assertStatusCode(HttpStatus.SC_OK); + assertThat(resp.getBody(), containsString("sampleUpdated")); + + resp = client.postJson(SAMPLE_RESOURCE_SEARCH_ENDPOINT, searchAllPayload()); + resp.assertStatusCode(HttpStatus.SC_OK); + + resp = client.postJson(SAMPLE_RESOURCE_SEARCH_ENDPOINT, searchByNamePayload("sampleUpdated")); + resp.assertStatusCode(HttpStatus.SC_OK); + + // delete ok + resp = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + adminResId); + resp.assertStatusCode(HttpStatus.SC_OK); + } + } +} diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/ProtectedTypesSettingTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/ProtectedTypesSettingTests.java new file mode 100644 index 0000000000..df25522ead --- /dev/null +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/ProtectedTypesSettingTests.java @@ -0,0 +1,446 @@ +/* + * 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.sample.resource.feature; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import com.carrotsearch.randomizedtesting.RandomizedRunner; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.HttpStatus; +import org.awaitility.Awaitility; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.sample.resource.TestUtils; +import org.opensearch.security.spi.resources.sharing.Recipient; +import org.opensearch.security.spi.resources.sharing.Recipients; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.test.framework.TestSecurityConfig; +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.equalTo; +import static org.opensearch.sample.resource.TestUtils.ApiHelper.searchAllPayload; +import static org.opensearch.sample.resource.TestUtils.ApiHelper.searchByNamePayload; +import static org.opensearch.sample.resource.TestUtils.FULL_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.LIMITED_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.NO_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.RESOURCE_SHARING_MIGRATION_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_FULL_ACCESS_RESOURCE_AG; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_READ_ONLY_RESOURCE_AG; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_CREATE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_DELETE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_GET_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SEARCH_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_UPDATE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SECURITY_SHARE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.migrationPayload_valid; +import static org.opensearch.sample.resource.TestUtils.newCluster; +import static org.opensearch.sample.resource.TestUtils.putSharingInfoPayload; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; +import static org.awaitility.Awaitility.await; + +/** + * Test the dynamic nature of feature settings: + * {@link ConfigConstants#OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES} + * + * Phase 1: empty list + * Phase 2: add sample resource as an entry + */ +@RunWith(RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class ProtectedTypesSettingTests { + + // do not include sample resource as protected resource, should behave as if feature was disabled for that resource + @ClassRule + public static LocalCluster cluster = newCluster(true, true, List.of()); + + private final TestUtils.ApiHelper api = new TestUtils.ApiHelper(cluster); + private String adminResId; + + // --------- Lifecycle --------- + + @Before + public void setup() { + // Starting with empty for deterministic ordering + removeSampleResourceAsProtectedType(); + awaitSetting(""); + + adminResId = createSampleResourceAs(USER_ADMIN); + } + + @After + public void cleanup() { + api.wipeOutResourceEntries(); + // Mark resource as not protected + removeSampleResourceAsProtectedType(); + awaitSetting(""); + } + + private String createSampleResourceAs(TestSecurityConfig.User user) { + try (TestRestClient client = cluster.getRestClient(user)) { + String sampleResource = """ + { + "name":"sample", + "store_user": true + } + """; + + TestRestClient.HttpResponse response = client.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, sampleResource); + + String resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim(); + + Awaitility.await() + .alias("Wait until resource data is populated") + .until(() -> client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId).getStatusCode(), equalTo(200)); + return resourceId; + } + } + + // --------- Phase 1: Resource not protected --------- + + @Test + public void testResourceNotProtected() { + // Not marked as protected type; expectations: + // - Share api handler not exist -> 501 on endpoints that would be implemented by the feature + // - Access relies purely on existing index/cluster perms ("legacy" RBAC behavior) + + assertNoAccessUser_LegacyProtection(); + assertLimitedUser_LegacyProtection(); + assertFullUser_LegacyProtection(); + assertAdminCert_LegacyProtection(); + } + + // --------- Assertions: Disabled behavior --------- + + private void assertNoAccessUser_LegacyProtection() { + // cannot create + try (TestRestClient c = cluster.getRestClient(NO_ACCESS_USER)) { + TestRestClient.HttpResponse r = c.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, "{\"name\":\"sampleUser\"}"); + r.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + // cannot get, update, delete + api.assertApiGet(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiUpdate(adminResId, NO_ACCESS_USER, "x", HttpStatus.SC_FORBIDDEN); + api.assertApiDelete(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + // share/revoke endpoints exist? -> when not protected we expect 400 since no corresponding types exist in ResourcePluginInfo + api.assertApiShare(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_BAD_REQUEST); + api.assertApiRevoke(adminResId, NO_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_BAD_REQUEST); + + // search forbidden + api.assertApiGetSearchForbidden(NO_ACCESS_USER); + api.assertApiPostSearchForbidden(searchAllPayload(), NO_ACCESS_USER); + api.assertApiPostSearchForbidden(searchByNamePayload("sample"), NO_ACCESS_USER); + } + + private void assertLimitedUser_LegacyProtection() { + // create own + String userResId = createSampleResourceAs(LIMITED_ACCESS_USER); + + // see admin resource (disabled path follows index perms) per your “disabled” expectations + api.assertApiGet(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sample"); + api.assertApiGetAll(LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sample"); + + // cannot update (no perm); cannot delete + api.assertApiUpdate(adminResId, LIMITED_ACCESS_USER, "x", HttpStatus.SC_FORBIDDEN); + api.assertApiUpdate(userResId, LIMITED_ACCESS_USER, "x", HttpStatus.SC_FORBIDDEN); + api.assertApiDelete(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + api.assertApiDelete(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + + // share/revoke endpoints exist? -> when not protected we expect 400 since no corresponding types exist in ResourcePluginInfo + api.assertApiShare(adminResId, LIMITED_ACCESS_USER, LIMITED_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_BAD_REQUEST); + api.assertApiRevoke(adminResId, LIMITED_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_BAD_REQUEST); + + // can search both resources + api.assertApiGetSearch(LIMITED_ACCESS_USER, HttpStatus.SC_OK, 2, "sample"); + api.assertApiPostSearch(searchAllPayload(), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 2, "sample"); + api.assertApiPostSearch(searchByNamePayload("sample"), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 2, "sample"); + } + + private void assertFullUser_LegacyProtection() { + String userResId = createSampleResourceAs(FULL_ACCESS_USER); + + // full * perms when disabled -> can see & update both + api.assertApiGet(adminResId, FULL_ACCESS_USER, HttpStatus.SC_OK, "sample"); + api.assertApiGetAll(FULL_ACCESS_USER, HttpStatus.SC_OK, "sample"); + api.assertApiUpdate(adminResId, FULL_ACCESS_USER, "sampleUpdateAdmin", HttpStatus.SC_OK); + api.assertApiUpdate(userResId, FULL_ACCESS_USER, "sampleUpdateUser", HttpStatus.SC_OK); + + // share/revoke endpoints exist? -> when not protected we expect 400 since no corresponding types exist in ResourcePluginInfo + api.assertApiShare(adminResId, FULL_ACCESS_USER, FULL_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_BAD_REQUEST); + api.assertApiRevoke(adminResId, FULL_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_BAD_REQUEST); + + // search sees both + api.assertApiGetSearch(FULL_ACCESS_USER, HttpStatus.SC_OK, 3, "sampleUpdateAdmin"); // admin + full user + limited user created + // above + api.assertApiPostSearch(searchAllPayload(), FULL_ACCESS_USER, HttpStatus.SC_OK, 3, "sampleUpdateAdmin"); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateAdmin"), FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateAdmin"); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateUser"), FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // can delete both + api.assertApiDelete(userResId, FULL_ACCESS_USER, HttpStatus.SC_OK); + api.assertApiDelete(adminResId, FULL_ACCESS_USER, HttpStatus.SC_OK); + } + + private void assertAdminCert_LegacyProtection() { + adminResId = createSampleResourceAs(USER_ADMIN); + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // read / update ok + TestRestClient.HttpResponse resp = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + adminResId); + resp.assertStatusCode(HttpStatus.SC_OK); + + resp = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + adminResId, "{\"name\":\"sampleUpdated\"}"); + resp.assertStatusCode(HttpStatus.SC_OK); + + // share/revoke endpoints exist? -> when not protected we expect 400 since no corresponding types exist in ResourcePluginInfo + resp = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + Recipient.USERS, + FULL_ACCESS_USER.getName() + ) + ); + resp.assertStatusCode(HttpStatus.SC_BAD_REQUEST); + + TestUtils.PatchSharingInfoPayloadBuilder payloadBuilder = new TestUtils.PatchSharingInfoPayloadBuilder(); + payloadBuilder.resourceId(adminResId); + payloadBuilder.resourceType(RESOURCE_TYPE); + payloadBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(FULL_ACCESS_USER.getName()))), + SAMPLE_FULL_ACCESS_RESOURCE_AG + ); + resp = client.patch(SECURITY_SHARE_ENDPOINT, payloadBuilder.build()); + resp.assertStatusCode(HttpStatus.SC_BAD_REQUEST); + + // search works + resp = client.get(SAMPLE_RESOURCE_SEARCH_ENDPOINT); + resp.assertStatusCode(HttpStatus.SC_OK); + + // delete ok + resp = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + adminResId); + resp.assertStatusCode(HttpStatus.SC_OK); + } + } + + // --------- Phase 2: Mark resource as protected --------- + + @Test + public void testResourceProtected() { + // Mark sample resource as protected + addSampleResourceAsProtectedType(); + awaitSetting(RESOURCE_TYPE); + + // migrate existing resources to new records + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + TestRestClient.HttpResponse migrateResponse = client.postJson(RESOURCE_SHARING_MIGRATION_ENDPOINT, migrationPayload_valid()); + migrateResponse.assertStatusCode(HttpStatus.SC_OK); + assertThat(migrateResponse.bodyAsMap().get("summary"), equalTo("Migration complete. migrated 1; skippedNoUser 0; failed 0")); + } + + // Marked as protected type; expectations: + // - Share/Revoke handlers -> return permission-based 200/403 (not 400) + // - Search and read access follow resource-sharing rules + // - Owners can share/revoke; others constrained + + assertNoAccessUser_ResourceProtection(); + assertLimitedUser_ResourceProtection(); + assertFullUser_ResourceProtection(); + assertAdminCert_ResourceProtection(); + } + + // --------- Helpers: cluster setting flips & awaits --------- + private void addSampleResourceAsProtectedType() { + String body = String.format( + "{\"transient\":{\"%s\":[\"%s\"]}}", + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES, + RESOURCE_TYPE + ); + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + TestRestClient.HttpResponse resp = client.putJson("_cluster/settings", body); + resp.assertStatusCode(HttpStatus.SC_OK); + } + } + + private void removeSampleResourceAsProtectedType() { + String body = String.format("{\"transient\":{\"%s\":[]}}", ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES); + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + TestRestClient.HttpResponse resp = client.putJson("_cluster/settings", body); + resp.assertStatusCode(HttpStatus.SC_OK); + } + } + + /** + * Confirm the setting took effect + */ + private void awaitSetting(String expected) { + // Wait for cluster setting to reflect desired value + await().atMost(30, TimeUnit.SECONDS).pollInterval(200, TimeUnit.MILLISECONDS).until(() -> readSettingEquals(expected)); + } + + private boolean readSettingEquals(String expected) { + try (var client = cluster.getRestClient(cluster.getAdminCertificate())) { + var r = client.get("_cluster/settings?include_defaults=true&flat_settings=true"); + r.assertStatusCode(200); + String expectedValue = expected.isEmpty() ? "[]" : ("[\"" + expected + "\"]"); + String key = "\"" + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES + "\":" + expectedValue; + return r.getBody().contains(key); + } catch (Exception e) { + return false; + } + } + + // --------- Assertions: Enabled behavior --------- + + private void assertNoAccessUser_ResourceProtection() { + // cannot create + try (TestRestClient c = cluster.getRestClient(NO_ACCESS_USER)) { + TestRestClient.HttpResponse r = c.putJson(SAMPLE_RESOURCE_CREATE_ENDPOINT, "{\"name\":\"sampleUser\"}"); + r.assertStatusCode(HttpStatus.SC_FORBIDDEN); + } + // cannot get/update/delete + api.assertApiGet(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiUpdate(adminResId, NO_ACCESS_USER, "x", HttpStatus.SC_FORBIDDEN); + api.assertApiDelete(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + + // share/revoke forbidden (handlers now exist → 403 vs 501) + api.assertApiShare(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertApiRevoke(adminResId, NO_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + + // search forbidden + api.assertApiGetSearchForbidden(NO_ACCESS_USER); + api.assertApiPostSearchForbidden(searchAllPayload(), NO_ACCESS_USER); + api.assertApiPostSearchForbidden(searchByNamePayload("sample"), NO_ACCESS_USER); + } + + private void assertLimitedUser_ResourceProtection() { + String userResId = createSampleResourceAs(LIMITED_ACCESS_USER); + + // cannot see admin resource under sharing rules + api.assertApiGet(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiGetAll(LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sample"); + + // can update own; not others + api.assertApiUpdate(adminResId, LIMITED_ACCESS_USER, "sampleUpdateAdmin", HttpStatus.SC_FORBIDDEN); + api.assertApiUpdate(userResId, LIMITED_ACCESS_USER, "sampleUpdateUser", HttpStatus.SC_OK); + api.assertApiGet(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sampleUpdateUser"); + api.assertApiGetSearch(LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // cannot share/revoke others, can share own + api.assertApiShare(adminResId, LIMITED_ACCESS_USER, LIMITED_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertApiRevoke(adminResId, LIMITED_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + + api.assertApiGet(userResId, USER_ADMIN, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiShare(userResId, LIMITED_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertApiGet(userResId, USER_ADMIN, HttpStatus.SC_OK, "sampleUpdateUser"); + api.assertApiRevoke(userResId, LIMITED_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertApiGet(userResId, USER_ADMIN, HttpStatus.SC_FORBIDDEN, ""); + + // searches aligned with ownership + api.assertApiGetSearch(LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiPostSearch(searchAllPayload(), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiPostSearch(searchByNamePayload("sample"), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 0, ""); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateUser"), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // can delete own + api.assertApiDelete(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK); + // cannot delete admin's + api.assertApiDelete(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + } + + private void assertFullUser_ResourceProtection() { + String userResId = createSampleResourceAs(FULL_ACCESS_USER); + + // even with * perms, sharing rules restrict access to others’ resources + api.assertApiGet(adminResId, FULL_ACCESS_USER, HttpStatus.SC_FORBIDDEN, "sample"); + api.assertApiGetAll(FULL_ACCESS_USER, HttpStatus.SC_OK, "sample"); + + // can update own + api.assertApiUpdate(adminResId, FULL_ACCESS_USER, "sampleUpdateAdmin", HttpStatus.SC_FORBIDDEN); + api.assertApiUpdate(userResId, FULL_ACCESS_USER, "sampleUpdateUser", HttpStatus.SC_OK); + api.assertApiGet(userResId, FULL_ACCESS_USER, HttpStatus.SC_OK, "sampleUpdateUser"); + api.assertApiGetSearch(FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // cannot share/revoke others’ resources; can share own + api.assertApiShare(adminResId, FULL_ACCESS_USER, FULL_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertApiRevoke(adminResId, FULL_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + + api.assertApiGet(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + api.assertApiShare(userResId, FULL_ACCESS_USER, LIMITED_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertApiGet(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sampleUpdateUser"); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateUser"), LIMITED_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiRevoke(userResId, FULL_ACCESS_USER, LIMITED_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertApiGet(userResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN, ""); + + // search visibility matches sharing state + api.assertApiGetSearch(FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiPostSearch(searchAllPayload(), FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + api.assertApiPostSearch(searchByNamePayload("sample"), FULL_ACCESS_USER, HttpStatus.SC_OK, 0, ""); + api.assertApiPostSearch(searchByNamePayload("sampleUpdateUser"), FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUpdateUser"); + + // can delete own; cannot delete admin’s under sharing rules + api.assertApiDelete(userResId, FULL_ACCESS_USER, HttpStatus.SC_OK); + api.assertApiDelete(adminResId, FULL_ACCESS_USER, HttpStatus.SC_FORBIDDEN); + } + + private void assertAdminCert_ResourceProtection() { + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + // read/update + TestRestClient.HttpResponse resp = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + adminResId); + resp.assertStatusCode(HttpStatus.SC_OK); + + resp = client.postJson(SAMPLE_RESOURCE_UPDATE_ENDPOINT + "/" + adminResId, "{\"name\":\"sampleUpdated\"}"); + resp.assertStatusCode(HttpStatus.SC_OK); + assertThat(resp.getBody(), containsString("sampleUpdated")); + + // share/revoke handlers exist → expect 200 for admin path + resp = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_FULL_ACCESS_RESOURCE_AG, Recipient.USERS, NO_ACCESS_USER.getName()) + ); + resp.assertStatusCode(HttpStatus.SC_OK); + + TestUtils.PatchSharingInfoPayloadBuilder payloadBuilder = new TestUtils.PatchSharingInfoPayloadBuilder(); + payloadBuilder.resourceId(adminResId); + payloadBuilder.resourceType(RESOURCE_TYPE); + payloadBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(NO_ACCESS_USER.getName()))), + SAMPLE_FULL_ACCESS_RESOURCE_AG + ); + resp = client.patch(SECURITY_SHARE_ENDPOINT, payloadBuilder.build()); + resp.assertStatusCode(HttpStatus.SC_OK); + + // search works + resp = client.get(SAMPLE_RESOURCE_SEARCH_ENDPOINT); + resp.assertStatusCode(HttpStatus.SC_OK); + assertThat(resp.getBody(), containsString("sampleUpdated")); + + resp = client.postJson(SAMPLE_RESOURCE_SEARCH_ENDPOINT, searchAllPayload()); + resp.assertStatusCode(HttpStatus.SC_OK); + + resp = client.postJson(SAMPLE_RESOURCE_SEARCH_ENDPOINT, searchByNamePayload("sampleUpdated")); + resp.assertStatusCode(HttpStatus.SC_OK); + + // delete ok + resp = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + adminResId); + resp.assertStatusCode(HttpStatus.SC_OK); + } + } +} diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/disabled/ApiAccessTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/disabled/ApiAccessTests.java index fe5f37c5f5..8aa2f87107 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/disabled/ApiAccessTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/disabled/ApiAccessTests.java @@ -8,6 +8,9 @@ package org.opensearch.sample.resource.feature.disabled; +import java.util.Map; +import java.util.Set; + import com.carrotsearch.randomizedtesting.RandomizedRunner; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.http.HttpStatus; @@ -19,6 +22,8 @@ import org.junit.runners.Suite; import org.opensearch.sample.resource.TestUtils; +import org.opensearch.security.spi.resources.sharing.Recipient; +import org.opensearch.security.spi.resources.sharing.Recipients; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; @@ -30,19 +35,19 @@ import static org.opensearch.sample.resource.TestUtils.FULL_ACCESS_USER; import static org.opensearch.sample.resource.TestUtils.LIMITED_ACCESS_USER; import static org.opensearch.sample.resource.TestUtils.NO_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.PatchSharingInfoPayloadBuilder; import static org.opensearch.sample.resource.TestUtils.RESOURCE_SHARING_INDEX; import static org.opensearch.sample.resource.TestUtils.SAMPLE_FULL_ACCESS_RESOURCE_AG; import static org.opensearch.sample.resource.TestUtils.SAMPLE_READ_ONLY_RESOURCE_AG; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_CREATE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_DELETE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_GET_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_REVOKE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SEARCH_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_UPDATE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SECURITY_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.newCluster; -import static org.opensearch.sample.resource.TestUtils.revokeAccessPayload; -import static org.opensearch.sample.resource.TestUtils.shareWithPayload; +import static org.opensearch.sample.resource.TestUtils.putSharingInfoPayload; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** @@ -117,8 +122,8 @@ public void testApiAccess_noAccessUser() { // feature is disabled, and thus request is treated as normal request. // Since user doesn't have permission to the share and revoke endpoints they will receive 403s - api.assertApiShare(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); - api.assertApiRevoke(adminResId, NO_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertApiShare(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_NOT_IMPLEMENTED); + api.assertApiRevoke(adminResId, NO_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_NOT_IMPLEMENTED); // search returns 403 since user doesn't have access to invoke search api.assertApiGetSearchForbidden(NO_ACCESS_USER); @@ -243,17 +248,27 @@ public void testApiAccess_adminCertificateUsers() { assertThat(resp.getBody(), containsString("sampleUpdated")); // can't share or revoke, as handlers don't exist - resp = client.postJson( - SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + adminResId, - shareWithPayload(FULL_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) + resp = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + Recipient.USERS, + FULL_ACCESS_USER.getName() + ) ); resp.assertStatusCode(HttpStatus.SC_NOT_IMPLEMENTED); - resp = client.postJson( - SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + adminResId, - revokeAccessPayload(FULL_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) + PatchSharingInfoPayloadBuilder patchBuilder = new PatchSharingInfoPayloadBuilder(); + patchBuilder.resourceId(adminResId); + patchBuilder.resourceType(RESOURCE_TYPE); + patchBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(FULL_ACCESS_USER.getName()))), + SAMPLE_FULL_ACCESS_RESOURCE_AG ); + resp = client.patch(SECURITY_SHARE_ENDPOINT, patchBuilder.build()); resp.assertStatusCode(HttpStatus.SC_NOT_IMPLEMENTED); @@ -339,8 +354,8 @@ public void testApiAccess_noAccessUser() { api.assertApiGet(adminResId, USER_ADMIN, HttpStatus.SC_OK, "sample"); // feature is disabled, no handler's exist - api.assertApiShare(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); - api.assertApiRevoke(adminResId, NO_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertApiShare(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_NOT_IMPLEMENTED); + api.assertApiRevoke(adminResId, NO_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_NOT_IMPLEMENTED); // search returns 403 since user doesn't have access to invoke search api.assertApiGetSearchForbidden(NO_ACCESS_USER); @@ -468,16 +483,21 @@ public void testApiAccess_adminCertificateUser() { assertThat(resp.getBody(), containsString("sampleUpdated")); // can't share or revoke, as handlers don't exist - resp = client.postJson( - SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + id, - shareWithPayload(FULL_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) + resp = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload(id, RESOURCE_TYPE, SAMPLE_FULL_ACCESS_RESOURCE_AG, Recipient.USERS, FULL_ACCESS_USER.getName()) ); resp.assertStatusCode(HttpStatus.SC_NOT_IMPLEMENTED); - resp = client.postJson( - SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + id, - revokeAccessPayload(FULL_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) + PatchSharingInfoPayloadBuilder patchBuilder = new PatchSharingInfoPayloadBuilder(); + patchBuilder.resourceId(id); + patchBuilder.resourceType(RESOURCE_TYPE); + patchBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(FULL_ACCESS_USER.getName()))), + SAMPLE_FULL_ACCESS_RESOURCE_AG ); + + resp = client.patch(SECURITY_SHARE_ENDPOINT, patchBuilder.build()); resp.assertStatusCode(HttpStatus.SC_NOT_IMPLEMENTED); // can search resources diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/ApiAccessTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/ApiAccessTests.java index 3f30d73dd8..caa413ab1d 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/ApiAccessTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/ApiAccessTests.java @@ -8,6 +8,9 @@ package org.opensearch.sample.resource.feature.enabled; +import java.util.Map; +import java.util.Set; + import com.carrotsearch.randomizedtesting.RandomizedRunner; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.http.HttpStatus; @@ -19,6 +22,8 @@ import org.junit.runners.Suite; import org.opensearch.sample.resource.TestUtils; +import org.opensearch.security.spi.resources.sharing.Recipient; +import org.opensearch.security.spi.resources.sharing.Recipients; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; @@ -36,13 +41,12 @@ import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_CREATE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_DELETE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_GET_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_REVOKE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SEARCH_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_UPDATE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SECURITY_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.newCluster; -import static org.opensearch.sample.resource.TestUtils.revokeAccessPayload; -import static org.opensearch.sample.resource.TestUtils.shareWithPayload; +import static org.opensearch.sample.resource.TestUtils.putSharingInfoPayload; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** @@ -243,17 +247,27 @@ public void testApiAccess_superAdmin() { assertThat(resp.getBody(), containsString("sampleUpdated")); // can share and revoke admin's resource - resp = client.postJson( - SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + adminResId, - shareWithPayload(NO_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) + resp = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + Recipient.USERS, + NO_ACCESS_USER.getName() + ) ); resp.assertStatusCode(HttpStatus.SC_OK); - resp = client.postJson( - SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + adminResId, - revokeAccessPayload(NO_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) + TestUtils.PatchSharingInfoPayloadBuilder payloadBuilder = new TestUtils.PatchSharingInfoPayloadBuilder(); + payloadBuilder.resourceId(adminResId); + payloadBuilder.resourceType(RESOURCE_TYPE); + payloadBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(NO_ACCESS_USER.getName()))), + SAMPLE_FULL_ACCESS_RESOURCE_AG ); + resp = client.patch(SECURITY_SHARE_ENDPOINT, payloadBuilder.build()); resp.assertStatusCode(HttpStatus.SC_OK); @@ -460,17 +474,27 @@ public void testApiAccess_superAdmin() { assertThat(resp.getBody(), containsString("sampleUpdated")); // can share and revoke admin's resource - resp = client.postJson( - SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + adminResId, - shareWithPayload(NO_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) + resp = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + Recipient.USERS, + NO_ACCESS_USER.getName() + ) ); resp.assertStatusCode(HttpStatus.SC_OK); - resp = client.postJson( - SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + adminResId, - revokeAccessPayload(NO_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) + TestUtils.PatchSharingInfoPayloadBuilder payloadBuilder = new TestUtils.PatchSharingInfoPayloadBuilder(); + payloadBuilder.resourceId(adminResId); + payloadBuilder.resourceType(RESOURCE_TYPE); + payloadBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(NO_ACCESS_USER.getName()))), + SAMPLE_FULL_ACCESS_RESOURCE_AG ); + resp = client.patch(SECURITY_SHARE_ENDPOINT, payloadBuilder.build()); resp.assertStatusCode(HttpStatus.SC_OK); diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/DirectIndexAccessTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/DirectIndexAccessTests.java index f51e693ac9..d6157376ea 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/DirectIndexAccessTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/DirectIndexAccessTests.java @@ -80,8 +80,7 @@ private void assertResourceIndexAccess(String id, TestSecurityConfig.User user) private void assertResourceSharingIndexAccess(String id, TestSecurityConfig.User user) { // cannot interact with resource sharing index api.assertDirectViewSharingRecord(id, user, HttpStatus.SC_FORBIDDEN); - api.assertDirectShare(id, user, user, SAMPLE_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); - api.assertDirectRevoke(id, user, user, SAMPLE_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertDirectUpdateSharingInfo(id, user, user, SAMPLE_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); api.assertDirectDeleteResourceSharingRecord(id, user, HttpStatus.SC_FORBIDDEN); } @@ -134,8 +133,13 @@ public void testRawAccess_allAccessUser() { // cannot interact with resource sharing index api.assertDirectViewSharingRecord(id, FULL_ACCESS_USER, HttpStatus.SC_NOT_FOUND); - api.assertDirectShare(id, FULL_ACCESS_USER, FULL_ACCESS_USER, SAMPLE_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); - api.assertDirectRevoke(id, FULL_ACCESS_USER, FULL_ACCESS_USER, SAMPLE_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertDirectUpdateSharingInfo( + id, + FULL_ACCESS_USER, + FULL_ACCESS_USER, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + HttpStatus.SC_FORBIDDEN + ); api.assertDirectDeleteResourceSharingRecord(id, FULL_ACCESS_USER, HttpStatus.SC_FORBIDDEN); } @@ -213,8 +217,13 @@ public void testRawAccess_noAccessUser() { // cannot interact with resource sharing index api.assertDirectViewSharingRecord(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN); - api.assertDirectShare(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); - api.assertDirectRevoke(adminResId, NO_ACCESS_USER, NO_ACCESS_USER, SAMPLE_FULL_ACCESS_RESOURCE_AG, HttpStatus.SC_FORBIDDEN); + api.assertDirectUpdateSharingInfo( + adminResId, + NO_ACCESS_USER, + NO_ACCESS_USER, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + HttpStatus.SC_FORBIDDEN + ); api.assertDirectDeleteResourceSharingRecord(adminResId, NO_ACCESS_USER, HttpStatus.SC_FORBIDDEN); } @@ -230,7 +239,7 @@ public void testRawAccess_limitedAccessUser() { } api.assertDirectGet(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sample"); // once admin share's record, user can then query it directly - api.assertDirectShare(adminResId, USER_ADMIN, LIMITED_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertDirectUpdateSharingInfo(adminResId, USER_ADMIN, LIMITED_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); api.awaitSharingEntry(adminResId, LIMITED_ACCESS_USER.getName()); api.assertDirectGet(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_OK, "sample"); @@ -245,14 +254,7 @@ public void testRawAccess_limitedAccessUser() { // cannot access resource sharing index since user doesn't have permissions on that index api.assertDirectViewSharingRecord(adminResId, LIMITED_ACCESS_USER, HttpStatus.SC_FORBIDDEN); - api.assertDirectShare( - adminResId, - LIMITED_ACCESS_USER, - LIMITED_ACCESS_USER, - SAMPLE_FULL_ACCESS_RESOURCE_AG, - HttpStatus.SC_FORBIDDEN - ); - api.assertDirectRevoke( + api.assertDirectUpdateSharingInfo( adminResId, LIMITED_ACCESS_USER, LIMITED_ACCESS_USER, @@ -276,12 +278,12 @@ public void testRawAccess_allAccessUser() { } api.assertDirectGet(adminResId, FULL_ACCESS_USER, HttpStatus.SC_OK, "sample"); // once admin share's record, user can then query it directly - api.assertDirectShare(adminResId, USER_ADMIN, FULL_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertDirectUpdateSharingInfo(adminResId, USER_ADMIN, FULL_ACCESS_USER, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); api.awaitSharingEntry(adminResId, FULL_ACCESS_USER.getName()); api.assertDirectGet(adminResId, FULL_ACCESS_USER, HttpStatus.SC_OK, "sample"); api.assertDirectGet(userResId, USER_ADMIN, HttpStatus.SC_OK, "sample"); - api.assertDirectShare(userResId, FULL_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); + api.assertDirectUpdateSharingInfo(userResId, FULL_ACCESS_USER, USER_ADMIN, SAMPLE_READ_ONLY_RESOURCE_AG, HttpStatus.SC_OK); api.assertDirectGet(userResId, USER_ADMIN, HttpStatus.SC_OK, "sample"); api.assertDirectGetSearch(FULL_ACCESS_USER, HttpStatus.SC_OK, 2, "sample"); @@ -289,12 +291,24 @@ public void testRawAccess_allAccessUser() { api.assertDirectPostSearch(searchByNamePayload("sample"), FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sample"); api.assertDirectPostSearch(searchByNamePayload("sampleUser"), FULL_ACCESS_USER, HttpStatus.SC_OK, 1, "sampleUser"); - // cannot update or delete resource - api.assertDirectUpdate(adminResId, FULL_ACCESS_USER, "sampleUpdateAdmin", HttpStatus.SC_OK); - api.assertDirectDelete(adminResId, FULL_ACCESS_USER, HttpStatus.SC_OK); // can update and delete own resource api.assertDirectUpdate(userResId, FULL_ACCESS_USER, "sampleUpdateUser", HttpStatus.SC_OK); api.assertDirectDelete(userResId, FULL_ACCESS_USER, HttpStatus.SC_OK); + + // can view, share, revoke and delete resource sharing record(s) directly + api.assertDirectViewSharingRecord(adminResId, FULL_ACCESS_USER, HttpStatus.SC_OK); + api.assertDirectUpdateSharingInfo( + adminResId, + FULL_ACCESS_USER, + NO_ACCESS_USER, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + HttpStatus.SC_OK + ); + api.assertDirectDeleteResourceSharingRecord(adminResId, FULL_ACCESS_USER, HttpStatus.SC_OK); + + // can update or delete admin resource, since system index protection is disabled and user has direct index access. + api.assertDirectUpdate(adminResId, FULL_ACCESS_USER, "sampleUpdateAdmin", HttpStatus.SC_OK); + api.assertDirectDelete(adminResId, FULL_ACCESS_USER, HttpStatus.SC_OK); } @Test diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/DryRunAccessTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/DryRunAccessTests.java index 2021811fe3..9785f85213 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/DryRunAccessTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/DryRunAccessTests.java @@ -9,6 +9,8 @@ package org.opensearch.sample.resource.feature.enabled; import java.util.List; +import java.util.Map; +import java.util.Set; import com.carrotsearch.randomizedtesting.RandomizedRunner; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; @@ -20,6 +22,8 @@ import org.junit.runner.RunWith; import org.opensearch.sample.resource.TestUtils; +import org.opensearch.security.spi.resources.sharing.Recipient; +import org.opensearch.security.spi.resources.sharing.Recipients; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; @@ -29,18 +33,18 @@ import static org.hamcrest.Matchers.equalTo; import static org.opensearch.sample.resource.TestUtils.FULL_ACCESS_USER; import static org.opensearch.sample.resource.TestUtils.NO_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.PatchSharingInfoPayloadBuilder; import static org.opensearch.sample.resource.TestUtils.RESOURCE_SHARING_INDEX; import static org.opensearch.sample.resource.TestUtils.SAMPLE_FULL_ACCESS_RESOURCE_AG; import static org.opensearch.sample.resource.TestUtils.SAMPLE_READ_ONLY_RESOURCE_AG; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_CREATE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_DELETE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_GET_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_REVOKE_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_UPDATE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SECURITY_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.newCluster; -import static org.opensearch.sample.resource.TestUtils.revokeAccessPayload; -import static org.opensearch.sample.resource.TestUtils.shareWithPayload; +import static org.opensearch.sample.resource.TestUtils.putSharingInfoPayload; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** @@ -121,22 +125,26 @@ public void testDryRunAccess() { assertThat(resp.bodyAsMap().get("missingPrivileges"), equalTo(List.of("cluster:admin/sample-resource-plugin/update"))); // cannot share resource - resp = client.postJson( - SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + adminResId + "?perform_permission_check=true", - shareWithPayload(FULL_ACCESS_USER.getName(), SAMPLE_READ_ONLY_RESOURCE_AG) + resp = client.putJson( + SECURITY_SHARE_ENDPOINT + "?perform_permission_check=true", + putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_READ_ONLY_RESOURCE_AG, Recipient.USERS, FULL_ACCESS_USER.getName()) ); resp.assertStatusCode(HttpStatus.SC_OK); assertThat(resp.bodyAsMap().get("accessAllowed"), equalTo(false)); - assertThat(resp.bodyAsMap().get("missingPrivileges"), equalTo(List.of("cluster:admin/sample-resource-plugin/share"))); + assertThat(resp.bodyAsMap().get("missingPrivileges"), equalTo(List.of("cluster:admin/security/resource/share"))); // cannot revoke resource access - resp = client.postJson( - SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + adminResId + "?perform_permission_check=true", - revokeAccessPayload(FULL_ACCESS_USER.getName(), SAMPLE_READ_ONLY_RESOURCE_AG) + PatchSharingInfoPayloadBuilder payloadBuilder = new PatchSharingInfoPayloadBuilder(); + payloadBuilder.resourceId(adminResId); + payloadBuilder.resourceType(RESOURCE_TYPE); + payloadBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(FULL_ACCESS_USER.getName()))), + SAMPLE_READ_ONLY_RESOURCE_AG ); + resp = client.patch(SECURITY_SHARE_ENDPOINT + "?perform_permission_check=true", payloadBuilder.build()); resp.assertStatusCode(HttpStatus.SC_OK); assertThat(resp.bodyAsMap().get("accessAllowed"), equalTo(false)); - assertThat(resp.bodyAsMap().get("missingPrivileges"), equalTo(List.of("cluster:admin/sample-resource-plugin/revoke"))); + assertThat(resp.bodyAsMap().get("missingPrivileges"), equalTo(List.of("cluster:admin/security/resource/share"))); // cannot delete resource resp = client.delete(SAMPLE_RESOURCE_DELETE_ENDPOINT + "/" + adminResId + "?perform_permission_check=true"); @@ -164,19 +172,23 @@ public void testDryRunAccess() { assertThat(resp.bodyAsMap().get("missingPrivileges"), equalTo(List.of())); // can share resource - resp = client.postJson( - SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + adminResId + "?perform_permission_check=true", - shareWithPayload(FULL_ACCESS_USER.getName(), SAMPLE_READ_ONLY_RESOURCE_AG) + resp = client.putJson( + SECURITY_SHARE_ENDPOINT + "?perform_permission_check=true", + putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_READ_ONLY_RESOURCE_AG, Recipient.USERS, FULL_ACCESS_USER.getName()) ); resp.assertStatusCode(HttpStatus.SC_OK); assertThat(resp.bodyAsMap().get("accessAllowed"), equalTo(true)); assertThat(resp.bodyAsMap().get("missingPrivileges"), equalTo(List.of())); // can revoke resource access - resp = client.postJson( - SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + adminResId + "?perform_permission_check=true", - revokeAccessPayload(FULL_ACCESS_USER.getName(), SAMPLE_READ_ONLY_RESOURCE_AG) + PatchSharingInfoPayloadBuilder payloadBuilder = new PatchSharingInfoPayloadBuilder(); + payloadBuilder.resourceId(adminResId); + payloadBuilder.resourceType(RESOURCE_TYPE); + payloadBuilder.revoke( + new Recipients(Map.of(Recipient.USERS, Set.of(FULL_ACCESS_USER.getName()))), + SAMPLE_READ_ONLY_RESOURCE_AG ); + resp = client.patch(SECURITY_SHARE_ENDPOINT + "?perform_permission_check=true", payloadBuilder.build()); resp.assertStatusCode(HttpStatus.SC_OK); assertThat(resp.bodyAsMap().get("accessAllowed"), equalTo(true)); assertThat(resp.bodyAsMap().get("missingPrivileges"), equalTo(List.of())); diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/ExcludedResourceTypeTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/ExcludedResourceTypeTests.java index a62a0d3762..f778f6dd78 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/ExcludedResourceTypeTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/ExcludedResourceTypeTests.java @@ -25,7 +25,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.not; import static org.opensearch.sample.resource.TestUtils.FULL_ACCESS_USER; import static org.opensearch.sample.resource.TestUtils.LIMITED_ACCESS_USER; import static org.opensearch.sample.resource.TestUtils.NO_ACCESS_USER; @@ -58,11 +57,12 @@ public void cleanup() { } @Test - public void testSampleResourceSharingIndexDoesNotExist() { + public void testSampleResourceSharingIndexExists() { + // we create resource-sharing index as we need to add index operation listener and we cannot add that dynamically try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { TestRestClient.HttpResponse response = client.get("_cat/indices?expand_wildcards=all"); response.assertStatusCode(HttpStatus.SC_OK); - assertThat(response.getBody(), not(containsString(RESOURCE_SHARING_INDEX))); + assertThat(response.getBody(), containsString(RESOURCE_SHARING_INDEX)); } } diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/multi_share/AdminCertificateAccessTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/multi_share/AdminCertificateAccessTests.java index 66e4ba4758..d5556ff9e0 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/multi_share/AdminCertificateAccessTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/multi_share/AdminCertificateAccessTests.java @@ -8,6 +8,9 @@ package org.opensearch.sample.resource.feature.enabled.multi_share; +import java.util.Map; +import java.util.Set; + import com.carrotsearch.randomizedtesting.RandomizedRunner; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.http.HttpStatus; @@ -16,6 +19,8 @@ import org.junit.runner.RunWith; import org.opensearch.sample.resource.TestUtils; +import org.opensearch.security.spi.resources.sharing.Recipient; +import org.opensearch.security.spi.resources.sharing.Recipients; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; @@ -23,15 +28,15 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.opensearch.sample.resource.TestUtils.NO_ACCESS_USER; +import static org.opensearch.sample.resource.TestUtils.PatchSharingInfoPayloadBuilder; import static org.opensearch.sample.resource.TestUtils.SAMPLE_FULL_ACCESS_RESOURCE_AG; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_DELETE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_GET_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_REVOKE_ENDPOINT; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_UPDATE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SECURITY_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.newCluster; -import static org.opensearch.sample.resource.TestUtils.revokeAccessPayload; -import static org.opensearch.sample.resource.TestUtils.shareWithPayload; +import static org.opensearch.sample.resource.TestUtils.putSharingInfoPayload; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** @@ -66,17 +71,18 @@ public void adminCertificate_canCRUD() { // can share and revoke admin's resource try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - HttpResponse response = client.postJson( - SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, - shareWithPayload(NO_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) + HttpResponse response = client.putJson( + SECURITY_SHARE_ENDPOINT, + putSharingInfoPayload(resourceId, RESOURCE_TYPE, SAMPLE_FULL_ACCESS_RESOURCE_AG, Recipient.USERS, NO_ACCESS_USER.getName()) ); response.assertStatusCode(HttpStatus.SC_OK); - response = client.postJson( - SAMPLE_RESOURCE_REVOKE_ENDPOINT + "/" + resourceId, - revokeAccessPayload(NO_ACCESS_USER.getName(), SAMPLE_FULL_ACCESS_RESOURCE_AG) - ); + PatchSharingInfoPayloadBuilder patchBuilder = new PatchSharingInfoPayloadBuilder(); + patchBuilder.resourceType(RESOURCE_TYPE); + patchBuilder.resourceId(resourceId); + patchBuilder.revoke(new Recipients(Map.of(Recipient.USERS, Set.of(NO_ACCESS_USER.getName()))), SAMPLE_FULL_ACCESS_RESOURCE_AG); + response = client.patch(SECURITY_SHARE_ENDPOINT, patchBuilder.build()); response.assertStatusCode(HttpStatus.SC_OK); } diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/multi_share/MixedAccessTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/multi_share/MixedAccessTests.java index 6655b7577c..5f10cd61a1 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/multi_share/MixedAccessTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/feature/enabled/multi_share/MixedAccessTests.java @@ -26,8 +26,9 @@ import static org.opensearch.sample.resource.TestUtils.LIMITED_ACCESS_USER; import static org.opensearch.sample.resource.TestUtils.SAMPLE_FULL_ACCESS_RESOURCE_AG; import static org.opensearch.sample.resource.TestUtils.SAMPLE_READ_ONLY_RESOURCE_AG; -import static org.opensearch.sample.resource.TestUtils.SAMPLE_RESOURCE_SHARE_ENDPOINT; +import static org.opensearch.sample.resource.TestUtils.SECURITY_SHARE_ENDPOINT; import static org.opensearch.sample.resource.TestUtils.newCluster; +import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; /** @@ -172,6 +173,8 @@ public void initialShare_multipleLevels() { String shareWithPayload = """ { + "resource_id": "%s", + "resource_type": "%s", "share_with": { "%s" : { "users": ["%s"] @@ -182,6 +185,8 @@ public void initialShare_multipleLevels() { } } """.formatted( + resourceId, + RESOURCE_TYPE, SAMPLE_FULL_ACCESS_RESOURCE_AG, LIMITED_ACCESS_USER.getName(), SAMPLE_READ_ONLY_RESOURCE_AG, @@ -189,7 +194,7 @@ public void initialShare_multipleLevels() { ); try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - TestRestClient.HttpResponse response = client.postJson(SAMPLE_RESOURCE_SHARE_ENDPOINT + "/" + resourceId, shareWithPayload); + TestRestClient.HttpResponse response = client.putJson(SECURITY_SHARE_ENDPOINT, shareWithPayload); response.assertStatusCode(HttpStatus.SC_OK); // wait for one of the users to be populated api.awaitSharingEntry(resourceId, FULL_ACCESS_USER.getName()); diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/AccessibleResourcesApiTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/AccessibleResourcesApiTests.java index 0d9ed56c4b..055f1abe37 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/AccessibleResourcesApiTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/AccessibleResourcesApiTests.java @@ -98,7 +98,7 @@ private void assertListApiWithUser(TestSecurityConfig.User user) { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { TestRestClient.HttpResponse response = client.putJson( SECURITY_SHARE_ENDPOINT, - putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_READ_ONLY_RESOURCE_AG, user.getName()) + putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_READ_ONLY_RESOURCE_AG, Recipient.USERS, user.getName()) ); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.getBody(), containsString(user.getName())); diff --git a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/ShareApiTests.java b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/ShareApiTests.java index b88983b30a..37d9b4c0a9 100644 --- a/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/ShareApiTests.java +++ b/sample-resource-plugin/src/integrationTest/java/org/opensearch/sample/resource/securityapis/ShareApiTests.java @@ -103,14 +103,14 @@ public void testGibberishPayload() { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { TestRestClient.HttpResponse response = client.putJson( SECURITY_SHARE_ENDPOINT, - putSharingInfoPayload("some-id", RESOURCE_TYPE, SAMPLE_READ_ONLY_RESOURCE_AG, NO_ACCESS_USER.getName()) + putSharingInfoPayload("some-id", RESOURCE_TYPE, SAMPLE_READ_ONLY_RESOURCE_AG, Recipient.USERS, NO_ACCESS_USER.getName()) ); response.assertStatusCode(HttpStatus.SC_FORBIDDEN); // since resource-index exists but resource-id doesn't, but user // shouldn't know that response = client.putJson( SECURITY_SHARE_ENDPOINT, - putSharingInfoPayload(adminResId, "some_type", SAMPLE_READ_ONLY_RESOURCE_AG, NO_ACCESS_USER.getName()) + putSharingInfoPayload(adminResId, "some_type", SAMPLE_READ_ONLY_RESOURCE_AG, Recipient.USERS, NO_ACCESS_USER.getName()) ); response.assertStatusCode(HttpStatus.SC_BAD_REQUEST); // since type doesn't exist, so does the corresponding index } @@ -146,7 +146,13 @@ public void testPutSharingInfo() { try (TestRestClient client = cluster.getRestClient(LIMITED_ACCESS_USER)) { TestRestClient.HttpResponse response = client.putJson( SECURITY_SHARE_ENDPOINT, - putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_READ_ONLY_RESOURCE_AG, NO_ACCESS_USER.getName()) + putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_READ_ONLY_RESOURCE_AG, + Recipient.USERS, + NO_ACCESS_USER.getName() + ) ); response.assertStatusCode(HttpStatus.SC_FORBIDDEN); } @@ -155,7 +161,13 @@ public void testPutSharingInfo() { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { TestRestClient.HttpResponse response = client.putJson( SECURITY_SHARE_ENDPOINT, - putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_FULL_ACCESS_RESOURCE_AG, LIMITED_ACCESS_USER.getName()) + putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + Recipient.USERS, + LIMITED_ACCESS_USER.getName() + ) ); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.getBody(), containsString(LIMITED_ACCESS_USER.getName())); @@ -166,7 +178,13 @@ public void testPutSharingInfo() { try (TestRestClient client = cluster.getRestClient(LIMITED_ACCESS_USER)) { TestRestClient.HttpResponse response = client.putJson( SECURITY_SHARE_ENDPOINT, - putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_READ_ONLY_RESOURCE_AG, NO_ACCESS_USER.getName()) + putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_READ_ONLY_RESOURCE_AG, + Recipient.USERS, + NO_ACCESS_USER.getName() + ) ); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.getBody(), containsString(NO_ACCESS_USER.getName())); @@ -187,7 +205,13 @@ public void testGetSharingInfo() { try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { TestRestClient.HttpResponse response = client.putJson( SECURITY_SHARE_ENDPOINT, - putSharingInfoPayload(adminResId, RESOURCE_TYPE, SAMPLE_FULL_ACCESS_RESOURCE_AG, FULL_ACCESS_USER.getName()) + putSharingInfoPayload( + adminResId, + RESOURCE_TYPE, + SAMPLE_FULL_ACCESS_RESOURCE_AG, + Recipient.USERS, + FULL_ACCESS_USER.getName() + ) ); response.assertStatusCode(HttpStatus.SC_OK); assertThat(response.getBody(), containsString(FULL_ACCESS_USER.getName())); diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java index 7a457485fc..e1b63cadd1 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/SampleResourcePlugin.java @@ -43,18 +43,12 @@ import org.opensearch.sample.resource.actions.rest.delete.DeleteResourceRestAction; import org.opensearch.sample.resource.actions.rest.get.GetResourceAction; import org.opensearch.sample.resource.actions.rest.get.GetResourceRestAction; -import org.opensearch.sample.resource.actions.rest.revoke.RevokeResourceAccessAction; -import org.opensearch.sample.resource.actions.rest.revoke.RevokeResourceAccessRestAction; import org.opensearch.sample.resource.actions.rest.search.SearchResourceAction; import org.opensearch.sample.resource.actions.rest.search.SearchResourceRestAction; -import org.opensearch.sample.resource.actions.rest.share.ShareResourceAction; -import org.opensearch.sample.resource.actions.rest.share.ShareResourceRestAction; import org.opensearch.sample.resource.actions.transport.CreateResourceTransportAction; import org.opensearch.sample.resource.actions.transport.DeleteResourceTransportAction; import org.opensearch.sample.resource.actions.transport.GetResourceTransportAction; -import org.opensearch.sample.resource.actions.transport.RevokeResourceAccessTransportAction; import org.opensearch.sample.resource.actions.transport.SearchResourceTransportAction; -import org.opensearch.sample.resource.actions.transport.ShareResourceTransportAction; import org.opensearch.sample.resource.actions.transport.UpdateResourceTransportAction; import org.opensearch.sample.secure.actions.rest.create.SecurePluginAction; import org.opensearch.sample.secure.actions.rest.create.SecurePluginRestAction; @@ -112,9 +106,6 @@ public List getRestHandlers( handlers.add(new DeleteResourceRestAction()); handlers.add(new SearchResourceRestAction()); - handlers.add(new ShareResourceRestAction()); - handlers.add(new RevokeResourceAccessRestAction()); - handlers.add(new SecurePluginRestAction()); return handlers; } @@ -127,8 +118,6 @@ public List getRestHandlers( actions.add(new ActionHandler<>(UpdateResourceAction.INSTANCE, UpdateResourceTransportAction.class)); actions.add(new ActionHandler<>(DeleteResourceAction.INSTANCE, DeleteResourceTransportAction.class)); actions.add(new ActionHandler<>(SearchResourceAction.INSTANCE, SearchResourceTransportAction.class)); - actions.add(new ActionHandler<>(ShareResourceAction.INSTANCE, ShareResourceTransportAction.class)); - actions.add(new ActionHandler<>(RevokeResourceAccessAction.INSTANCE, RevokeResourceAccessTransportAction.class)); actions.add(new ActionHandler<>(SecurePluginAction.INSTANCE, SecurePluginTransportAction.class)); return actions; } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessAction.java deleted file mode 100644 index 9231683499..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessAction.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.resource.actions.rest.revoke; - -import org.opensearch.action.ActionType; - -/** - * Action to revoke a sample resource - */ -public class RevokeResourceAccessAction extends ActionType { - /** - * Revoke sample resource action instance - */ - public static final RevokeResourceAccessAction INSTANCE = new RevokeResourceAccessAction(); - /** - * Revoke sample resource action name - */ - public static final String NAME = "cluster:admin/sample-resource-plugin/revoke"; - - private RevokeResourceAccessAction() { - super(NAME, RevokeResourceAccessResponse::new); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRequest.java deleted file mode 100644 index f47f9b1c63..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRequest.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.resource.actions.rest.revoke; - -import java.io.IOException; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.action.DocRequest; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.security.spi.resources.sharing.ShareWith; - -import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; -import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; - -/** - * Request object for revoking access to a sample resource - */ -public class RevokeResourceAccessRequest extends ActionRequest implements DocRequest { - - String resourceId; - ShareWith revokeAccess; - - public RevokeResourceAccessRequest(String resourceId, ShareWith entitiesToRevoke) { - this.resourceId = resourceId; - this.revokeAccess = entitiesToRevoke; - } - - public RevokeResourceAccessRequest(StreamInput in) throws IOException { - resourceId = in.readString(); - revokeAccess = new ShareWith(in); - } - - @Override - public void writeTo(final StreamOutput out) throws IOException { - out.writeString(resourceId); - revokeAccess.writeTo(out); - } - - @Override - public ActionRequestValidationException validate() { - return null; - } - - public String getResourceId() { - return resourceId; - } - - public ShareWith getEntitiesToRevoke() { - return revokeAccess; - } - - @Override - public String type() { - return RESOURCE_TYPE; - } - - @Override - public String index() { - return RESOURCE_INDEX_NAME; - } - - @Override - public String id() { - return resourceId; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessResponse.java deleted file mode 100644 index 2a1bf47e6f..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessResponse.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.resource.actions.rest.revoke; - -import java.io.IOException; - -import org.opensearch.core.action.ActionResponse; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.security.spi.resources.sharing.ShareWith; - -/** - * Response for the RevokeResourceAccessAction - */ -public class RevokeResourceAccessResponse extends ActionResponse implements ToXContentObject { - private final ShareWith shareWith; - - public RevokeResourceAccessResponse(ShareWith shareWith) { - this.shareWith = shareWith; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeNamedWriteable(shareWith); - } - - public RevokeResourceAccessResponse(final StreamInput in) throws IOException { - shareWith = new ShareWith(in); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field("share_with", shareWith); - builder.endObject(); - return builder; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java deleted file mode 100644 index f8b2000753..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/revoke/RevokeResourceAccessRestAction.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.resource.actions.rest.revoke; - -import java.io.IOException; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -import org.opensearch.core.common.Strings; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.action.RestToXContentListener; -import org.opensearch.security.spi.resources.sharing.Recipient; -import org.opensearch.security.spi.resources.sharing.Recipients; -import org.opensearch.security.spi.resources.sharing.ShareWith; -import org.opensearch.transport.client.node.NodeClient; - -import static java.util.Collections.singletonList; -import static org.opensearch.rest.RestRequest.Method.POST; -import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; - -/** - * Rest Action to revoke sample resource access - */ -public class RevokeResourceAccessRestAction extends BaseRestHandler { - - public RevokeResourceAccessRestAction() {} - - @Override - public List routes() { - return singletonList(new Route(POST, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/revoke/{resource_id}")); - } - - @Override - public String getName() { - return "revoke_sample_resource"; - } - - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - String resourceId = request.param("resource_id"); - if (Strings.isNullOrEmpty(resourceId)) { - throw new IllegalArgumentException("resource_id parameter is required"); - } - Map source; - try (XContentParser parser = request.contentParser()) { - source = parser.map(); - } - - Map revokeEntities = (Map) source.get("entities_to_revoke"); - - Map revokeRecipients = new HashMap<>(); - if (revokeEntities != null) { - Map> recipients; - for (Map.Entry entry : revokeEntities.entrySet()) { - String accessLevel = entry.getKey(); - Map recs = (Map) entry.getValue(); - recipients = new HashMap<>(); - for (Map.Entry rec : recs.entrySet()) { - Recipient recipient = Recipient.valueOf(rec.getKey().toUpperCase(Locale.ROOT)); - Set targets = new HashSet<>((Collection) rec.getValue()); - recipients.put(recipient, targets); - } - revokeRecipients.put(accessLevel, new Recipients(recipients)); - } - } - - final RevokeResourceAccessRequest revokeResourceAccessRequest = new RevokeResourceAccessRequest( - resourceId, - new ShareWith(revokeRecipients) - ); - return channel -> client.executeLocally( - RevokeResourceAccessAction.INSTANCE, - revokeResourceAccessRequest, - new RestToXContentListener<>(channel) - ); - } - -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceAction.java deleted file mode 100644 index 52de757b1b..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceAction.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.resource.actions.rest.share; - -import org.opensearch.action.ActionType; - -/** - * Action to share a sample resource - */ -public class ShareResourceAction extends ActionType { - /** - * Share sample resource action instance - */ - public static final ShareResourceAction INSTANCE = new ShareResourceAction(); - /** - * Share sample resource action name - */ - public static final String NAME = "cluster:admin/sample-resource-plugin/share"; - - private ShareResourceAction() { - super(NAME, ShareResourceResponse::new); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRequest.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRequest.java deleted file mode 100644 index 9ad7efd6b8..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRequest.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.resource.actions.rest.share; - -import java.io.IOException; - -import org.opensearch.action.ActionRequest; -import org.opensearch.action.ActionRequestValidationException; -import org.opensearch.action.DocRequest; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.security.spi.resources.sharing.ShareWith; - -import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; -import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE; - -/** - * Request object for sharing sample resource transport action - */ -public class ShareResourceRequest extends ActionRequest implements DocRequest { - - private final String resourceId; - - private final ShareWith shareWithRecipients; - - public ShareResourceRequest(String resourceId, ShareWith shareWithRecipients) { - this.resourceId = resourceId; - this.shareWithRecipients = shareWithRecipients; - } - - public ShareResourceRequest(StreamInput in) throws IOException { - this.resourceId = in.readString(); - this.shareWithRecipients = new ShareWith(in); - } - - @Override - public void writeTo(final StreamOutput out) throws IOException { - out.writeString(this.resourceId); - shareWithRecipients.writeTo(out); - } - - @Override - public ActionRequestValidationException validate() { - return null; - } - - public String getResourceId() { - return this.resourceId; - } - - public ShareWith getShareWith() { - return shareWithRecipients; - } - - @Override - public String type() { - return RESOURCE_TYPE; - } - - @Override - public String index() { - return RESOURCE_INDEX_NAME; - } - - @Override - public String id() { - return resourceId; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceResponse.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceResponse.java deleted file mode 100644 index e8df82b841..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceResponse.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.resource.actions.rest.share; - -import java.io.IOException; - -import org.opensearch.core.action.ActionResponse; -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.security.spi.resources.sharing.ShareWith; - -/** - * Response object for ShareResourceAction - */ -public class ShareResourceResponse extends ActionResponse implements ToXContentObject { - private final ShareWith shareWith; - - public ShareResourceResponse(ShareWith shareWith) { - this.shareWith = shareWith; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeNamedWriteable(shareWith); - } - - public ShareResourceResponse(final StreamInput in) throws IOException { - shareWith = new ShareWith(in); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field("share_with", shareWith); - builder.endObject(); - return builder; - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java deleted file mode 100644 index ceb7efc29e..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/rest/share/ShareResourceRestAction.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.resource.actions.rest.share; - -import java.io.IOException; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -import org.opensearch.core.common.Strings; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.rest.BaseRestHandler; -import org.opensearch.rest.RestRequest; -import org.opensearch.rest.action.RestToXContentListener; -import org.opensearch.security.spi.resources.sharing.Recipient; -import org.opensearch.security.spi.resources.sharing.Recipients; -import org.opensearch.security.spi.resources.sharing.ShareWith; -import org.opensearch.transport.client.node.NodeClient; - -import static java.util.Collections.singletonList; -import static org.opensearch.rest.RestRequest.Method.POST; -import static org.opensearch.sample.utils.Constants.SAMPLE_RESOURCE_PLUGIN_API_PREFIX; - -/** - * Rest Action to share a resource - */ -public class ShareResourceRestAction extends BaseRestHandler { - - public ShareResourceRestAction() {} - - @Override - public List routes() { - return singletonList(new Route(POST, SAMPLE_RESOURCE_PLUGIN_API_PREFIX + "/share/{resource_id}")); - } - - @Override - public String getName() { - return "share_sample_resource"; - } - - // NOTE: Do NOT use @SuppressWarnings("unchecked") on untrusted data in production code. This is used here only to keep the code simple - @SuppressWarnings("unchecked") - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - String resourceId = request.param("resource_id"); - if (Strings.isNullOrEmpty(resourceId)) { - throw new IllegalArgumentException("resource_id parameter is required"); - } - - Map source; - try (XContentParser parser = request.contentParser()) { - source = parser.map(); - } - - Map shareWith = (Map) source.get("share_with"); - - Map shareWithRecipients = new HashMap<>(); - if (shareWith != null) { - Map> recipients; - for (Map.Entry entry : shareWith.entrySet()) { - String accessLevel = entry.getKey(); - Map recs = (Map) entry.getValue(); - recipients = new HashMap<>(); - for (Map.Entry rec : recs.entrySet()) { - Recipient recipient = Recipient.valueOf(rec.getKey().toUpperCase(Locale.ROOT)); - Set targets = new HashSet<>((Collection) rec.getValue()); - recipients.put(recipient, targets); - } - shareWithRecipients.put(accessLevel, new Recipients(recipients)); - } - } - - final ShareResourceRequest shareResourceRequest = new ShareResourceRequest(resourceId, new ShareWith(shareWithRecipients)); - return channel -> client.executeLocally(ShareResourceAction.INSTANCE, shareResourceRequest, new RestToXContentListener<>(channel)); - } -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java index 3ab5e2fe59..126503faf8 100644 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java +++ b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/GetResourceTransportAction.java @@ -68,18 +68,16 @@ private void fetchAllResources(ActionListener listener) { SearchRequest req = new SearchRequest(RESOURCE_INDEX_NAME).source(ssb); pluginClient.search(req, ActionListener.wrap(searchResponse -> { SearchHit[] hits = searchResponse.getHits().getHits(); - if (hits.length == 0) { - listener.onFailure(new ResourceNotFoundException("No resources found in index: " + RESOURCE_INDEX_NAME)); - } else { - Set resources = Arrays.stream(hits).map(hit -> { - try { - return parseResource(hit.getSourceAsString()); - } catch (IOException e) { - throw new RuntimeException(e); - } - }).collect(Collectors.toSet()); - listener.onResponse(new GetResourceResponse(resources)); - } + + Set resources = Arrays.stream(hits).map(hit -> { + try { + return parseResource(hit.getSourceAsString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }).collect(Collectors.toSet()); + listener.onResponse(new GetResourceResponse(resources)); + }, listener::onFailure)); } diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java deleted file mode 100644 index af6d0fc9b5..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/RevokeResourceAccessTransportAction.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.resource.actions.transport; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.OpenSearchStatusException; -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.HandledTransportAction; -import org.opensearch.common.inject.Inject; -import org.opensearch.core.action.ActionListener; -import org.opensearch.core.rest.RestStatus; -import org.opensearch.sample.client.ResourceSharingClientAccessor; -import org.opensearch.sample.resource.actions.rest.revoke.RevokeResourceAccessAction; -import org.opensearch.sample.resource.actions.rest.revoke.RevokeResourceAccessRequest; -import org.opensearch.sample.resource.actions.rest.revoke.RevokeResourceAccessResponse; -import org.opensearch.security.spi.resources.client.ResourceSharingClient; -import org.opensearch.security.spi.resources.sharing.ShareWith; -import org.opensearch.tasks.Task; -import org.opensearch.transport.TransportService; - -import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; - -/** - * Transport action for revoking resource access. - */ -public class RevokeResourceAccessTransportAction extends HandledTransportAction { - private static final Logger log = LogManager.getLogger(RevokeResourceAccessTransportAction.class); - - private final ResourceSharingClient resourceSharingClient; - - @Inject - public RevokeResourceAccessTransportAction(TransportService transportService, ActionFilters actionFilters) { - super(RevokeResourceAccessAction.NAME, transportService, actionFilters, RevokeResourceAccessRequest::new); - this.resourceSharingClient = ResourceSharingClientAccessor.getInstance().getResourceSharingClient(); - } - - @Override - protected void doExecute(Task task, RevokeResourceAccessRequest request, ActionListener listener) { - if (resourceSharingClient == null) { - listener.onFailure( - new OpenSearchStatusException( - "Resource sharing is not enabled. Cannot revoke access to resource " + request.getResourceId(), - RestStatus.NOT_IMPLEMENTED - ) - ); - return; - } - ShareWith target = request.getEntitiesToRevoke(); - resourceSharingClient.revoke(request.getResourceId(), RESOURCE_INDEX_NAME, target, ActionListener.wrap(success -> { - RevokeResourceAccessResponse response = new RevokeResourceAccessResponse(success.getShareWith()); - log.debug("Revoked resource access: {}", response.toString()); - listener.onResponse(response); - }, listener::onFailure)); - } - -} diff --git a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java b/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java deleted file mode 100644 index 35482821bd..0000000000 --- a/sample-resource-plugin/src/main/java/org/opensearch/sample/resource/actions/transport/ShareResourceTransportAction.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.sample.resource.actions.transport; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.OpenSearchStatusException; -import org.opensearch.action.support.ActionFilters; -import org.opensearch.action.support.HandledTransportAction; -import org.opensearch.common.inject.Inject; -import org.opensearch.core.action.ActionListener; -import org.opensearch.core.rest.RestStatus; -import org.opensearch.sample.client.ResourceSharingClientAccessor; -import org.opensearch.sample.resource.actions.rest.share.ShareResourceAction; -import org.opensearch.sample.resource.actions.rest.share.ShareResourceRequest; -import org.opensearch.sample.resource.actions.rest.share.ShareResourceResponse; -import org.opensearch.security.spi.resources.client.ResourceSharingClient; -import org.opensearch.security.spi.resources.sharing.ShareWith; -import org.opensearch.tasks.Task; -import org.opensearch.transport.TransportService; - -import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME; - -/** - * Transport action implementation for sharing a resource. - */ -public class ShareResourceTransportAction extends HandledTransportAction { - private static final Logger log = LogManager.getLogger(ShareResourceTransportAction.class); - private final ResourceSharingClient resourceSharingClient; - - @Inject - public ShareResourceTransportAction(TransportService transportService, ActionFilters actionFilters) { - super(ShareResourceAction.NAME, transportService, actionFilters, ShareResourceRequest::new); - this.resourceSharingClient = ResourceSharingClientAccessor.getInstance().getResourceSharingClient(); - } - - @Override - protected void doExecute(Task task, ShareResourceRequest request, ActionListener listener) { - if (request.getResourceId() == null || request.getResourceId().isEmpty()) { - listener.onFailure(new IllegalArgumentException("Resource ID cannot be null or empty")); - return; - } - - if (resourceSharingClient == null) { - listener.onFailure( - new OpenSearchStatusException( - "Resource sharing is not enabled. Cannot share resource " + request.getResourceId(), - RestStatus.NOT_IMPLEMENTED - ) - ); - return; - } - ShareWith shareWith = request.getShareWith(); - resourceSharingClient.share(request.getResourceId(), RESOURCE_INDEX_NAME, shareWith, ActionListener.wrap(sharing -> { - ShareWith finalShareWith = sharing == null ? null : sharing.getShareWith(); - ShareResourceResponse response = new ShareResourceResponse(finalShareWith); - log.debug("Shared resource: {}", response.toString()); - listener.onResponse(response); - }, listener::onFailure)); - } - -} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/client/ResourceSharingClient.java b/spi/src/main/java/org/opensearch/security/spi/resources/client/ResourceSharingClient.java index 9a92020254..01ed81c906 100644 --- a/spi/src/main/java/org/opensearch/security/spi/resources/client/ResourceSharingClient.java +++ b/spi/src/main/java/org/opensearch/security/spi/resources/client/ResourceSharingClient.java @@ -54,4 +54,11 @@ public interface ResourceSharingClient { * @param listener The listener to be notified with the set of accessible resources. */ void getAccessibleResourceIds(String resourceIndex, ActionListener> listener); + + /** + * Returns a flag to indicate whether resource-sharing is enabled for resource-type + * @param resourceType the type for which resource-sharing status is to be checked + * @return true if enabled, false otherwise + */ + boolean isFeatureEnabledForType(String resourceType); } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index d380f0baa8..f2e1588ce3 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -189,6 +189,8 @@ import org.opensearch.security.resources.api.share.ShareAction; import org.opensearch.security.resources.api.share.ShareRestAction; import org.opensearch.security.resources.api.share.ShareTransportAction; +import org.opensearch.security.resources.settings.ResourceSharingFeatureFlagSetting; +import org.opensearch.security.resources.settings.ResourceSharingProtectedResourcesSetting; import org.opensearch.security.rest.DashboardsInfoAction; import org.opensearch.security.rest.SecurityConfigUpdateAction; import org.opensearch.security.rest.SecurityHealthAction; @@ -245,7 +247,6 @@ import static org.opensearch.security.resources.ResourceSharingIndexHandler.getSharingIndex; import static org.opensearch.security.setting.DeprecatedSettings.checkForDeprecatedSetting; import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER; -import static org.opensearch.security.support.ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT; import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX; import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE; import static org.opensearch.security.support.ConfigConstants.SECURITY_SSL_CERTIFICATES_HOT_RELOAD_ENABLED; @@ -295,7 +296,9 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile IndexResolverReplacer irr; private final AtomicReference namedXContentRegistry = new AtomicReference<>(NamedXContentRegistry.EMPTY);; private volatile DlsFlsRequestValve dlsFlsValve = null; - private volatile OpensearchDynamicSetting transportPassiveAuthSetting; + private final OpensearchDynamicSetting transportPassiveAuthSetting; + private final OpensearchDynamicSetting resourceSharingEnabledSetting; + private final OpensearchDynamicSetting> resourceSharingProtectedResourceTypesSetting; private volatile PasswordHasher passwordHasher; private volatile DlsFlsBaseContext dlsFlsBaseContext; private ResourceSharingIndexHandler rsIndexHandler; @@ -364,7 +367,12 @@ public OpenSearchSecurityPlugin(final Settings settings, final Path configPath) disabled = isDisabled(settings); sslCertReloadEnabled = isSslCertReloadEnabled(settings); + // dynamic settings transportPassiveAuthSetting = new TransportPassiveAuthSetting(settings); + resourceSharingEnabledSetting = new ResourceSharingFeatureFlagSetting(settings, resourcePluginInfo); // not filtered + resourceSharingProtectedResourceTypesSetting = new ResourceSharingProtectedResourcesSetting(settings, resourcePluginInfo); // not + // filtered + resourcePluginInfo.setResourceSharingProtectedTypesSetting(resourceSharingProtectedResourceTypesSetting); if (disabled) { this.sslCertReloadEnabled = false; @@ -646,10 +654,9 @@ public List getRestHandlers( ); handlers.add( new DashboardsInfoAction( - settings, - restController, Objects.requireNonNull(evaluator), - Objects.requireNonNull(threadPool) + Objects.requireNonNull(threadPool), + resourceSharingEnabledSetting ) ); handlers.add( @@ -706,14 +713,11 @@ public List getRestHandlers( ); // Resource sharing API to update sharing info - if (settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - )) { - handlers.add(new ShareRestAction(resourcePluginInfo)); - handlers.add(new ResourceTypesRestAction(resourcePluginInfo)); - handlers.add(new AccessibleResourcesRestAction(resourceAccessHandler, resourcePluginInfo)); - } + handlers.add( + new ShareRestAction(resourcePluginInfo, resourceSharingEnabledSetting, resourceSharingProtectedResourceTypesSetting) + ); + handlers.add(new ResourceTypesRestAction(resourcePluginInfo, resourceSharingEnabledSetting)); + handlers.add(new AccessibleResourcesRestAction(resourceAccessHandler, resourcePluginInfo, resourceSharingEnabledSetting)); } log.debug("Added {} rest handler(s)", handlers.size()); @@ -744,10 +748,7 @@ public UnaryOperator getRestHandlerWrapper(final ThreadContext thre actions.add(new ActionHandler<>(WhoAmIAction.INSTANCE, TransportWhoAmIAction.class)); // transport action to handle sharing info update - if (settings.getAsBoolean(ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT)) { - actions.add(new ActionHandler<>(ShareAction.INSTANCE, ShareTransportAction.class)); - } - + actions.add(new ActionHandler<>(ShareAction.INSTANCE, ShareTransportAction.class)); } return actions; } @@ -776,19 +777,20 @@ public void onIndexModule(IndexModule indexModule) { ) ); - if (settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - )) { - // Listening on POST and DELETE operations in resource indices - ResourceIndexListener resourceIndexListener = new ResourceIndexListener(threadPool, localClient, resourcePluginInfo); - // CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions - Set resourceIndices = resourcePluginInfo.getResourceIndices(); - // CS-ENFORCE-SINGLE - if (resourceIndices.contains(indexModule.getIndex().getName())) { - indexModule.addIndexOperationListener(resourceIndexListener); - log.info("Security plugin started listening to operations on resource-index {}", indexModule.getIndex().getName()); - } + // Listening on POST and DELETE operations in resource indices + ResourceIndexListener resourceIndexListener = new ResourceIndexListener( + threadPool, + localClient, + resourcePluginInfo, + resourceSharingEnabledSetting, + resourceSharingProtectedResourceTypesSetting + ); + // CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions + Set resourceIndices = resourcePluginInfo.getResourceIndices(); + // CS-ENFORCE-SINGLE + if (resourceIndices.contains(indexModule.getIndex().getName())) { + indexModule.addIndexOperationListener(resourceIndexListener); + log.info("Security plugin started listening to operations on resource-index {}", indexModule.getIndex().getName()); } indexModule.forceQueryCacheProvider((indexSettings, nodeCache) -> new QueryCache() { @@ -1135,6 +1137,8 @@ public Collection createComponents( // Register opensearch dynamic settings transportPassiveAuthSetting.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); + resourceSharingEnabledSetting.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); + resourceSharingProtectedResourceTypesSetting.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); final ClusterInfoHolder cih = new ClusterInfoHolder(this.cs.getClusterName().value()); this.cs.addListener(cih); @@ -1213,32 +1217,36 @@ public Collection createComponents( threadPool, dlsFlsBaseContext, adminDns, - resourcePluginInfo.getResourceIndices() + resourcePluginInfo, + resourceSharingEnabledSetting ); cr.subscribeOnChange(configMap -> { ((DlsFlsValveImpl) dlsFlsValve).updateConfiguration(cr.getConfiguration(CType.ROLES)); }); } resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDns, evaluator, resourcePluginInfo); - if (settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - )) { - // CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions - // Assign resource sharing client to each extension - // Using the non-gated client (i.e. no additional permissions required) - ResourceSharingClient resourceAccessControlClient = new ResourceAccessControlClient( - resourceAccessHandler, - resourcePluginInfo.getResourceIndices() - ); - resourcePluginInfo.getResourceSharingExtensions().forEach(extension -> { - extension.assignResourceSharingClient(resourceAccessControlClient); - }); - components.add(resourcePluginInfo); - components.add(resourceAccessHandler); - // CS-ENFORCE-SINGLE - } - resourceAccessEvaluator = new ResourceAccessEvaluator(resourcePluginInfo.getResourceIndices(), settings, resourceAccessHandler); + // CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions + // Assign resource sharing client to each extension + // Using the non-gated client (i.e. no additional permissions required) + ResourceSharingClient resourceAccessControlClient = new ResourceAccessControlClient( + resourceAccessHandler, + resourcePluginInfo, + resourceSharingProtectedResourceTypesSetting + ); + resourcePluginInfo.setResourceSharingClient(resourceAccessControlClient); + resourcePluginInfo.getResourceSharingExtensions().forEach(extension -> { + extension.assignResourceSharingClient(resourceAccessControlClient); + }); + components.add(resourcePluginInfo); + components.add(resourceAccessHandler); + // CS-ENFORCE-SINGLE + + resourceAccessEvaluator = new ResourceAccessEvaluator( + resourcePluginInfo, + resourceAccessHandler, + resourceSharingEnabledSetting, + resourceSharingProtectedResourceTypesSetting + ); sf = new SecurityFilter( settings, @@ -2231,26 +2239,11 @@ public List> getSettings() { settings.add(RoleBasedActionPrivileges.PRECOMPUTED_PRIVILEGES_ENABLED); // Resource Sharing - settings.add( - Setting.boolSetting( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT, - Property.NodeScope, - Property.Filtered - ) - ); + settings.add(resourceSharingEnabledSetting.getDynamicSetting()); // resource marked here will be protected, other resources will not be protected with resource sharing model // Defaults to no resources as protected - settings.add( - Setting.listSetting( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES_DEFAULT, - Function.identity(), - Property.NodeScope, - Property.Filtered - ) - ); + settings.add(resourceSharingProtectedResourceTypesSetting.getDynamicSetting()); settings.add(UserFactory.Caching.MAX_SIZE); settings.add(UserFactory.Caching.EXPIRE_AFTER_ACCESS); @@ -2321,11 +2314,7 @@ public void onNodeStarted(DiscoveryNode localNode) { // resourceSharingIndexManagementRepository will be null when sec plugin is disabled or is in SSLOnly mode, hence it will not be // instantiated - if (settings != null - && settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - )) { + if (resourceSharingEnabledSetting.getDynamicSettingValue()) { // create resource sharing index if absent // TODO check if this should be wrapped in an atomic completable future log.debug("Attempting to create Resource Sharing index"); @@ -2385,18 +2374,13 @@ public Collection getSystemIndexDescriptors(Settings sett ); final SystemIndexDescriptor securityIndexDescriptor = new SystemIndexDescriptor(indexPattern, "Security index"); systemIndexDescriptors.add(securityIndexDescriptor); - if (settings != null - && settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - )) { - for (String resourceIndex : resourcePluginInfo.getResourceIndices()) { - final SystemIndexDescriptor resourceSharingIndexDescriptor = new SystemIndexDescriptor( - getSharingIndex(resourceIndex), - "Resource Sharing index for index: " + resourceIndex - ); - systemIndexDescriptors.add(resourceSharingIndexDescriptor); - } + + for (String resourceIndex : resourcePluginInfo.getResourceIndices()) { + final SystemIndexDescriptor resourceSharingIndexDescriptor = new SystemIndexDescriptor( + getSharingIndex(resourceIndex), + "Resource Sharing index for index: " + resourceIndex + ); + systemIndexDescriptors.add(resourceSharingIndexDescriptor); } if (SecurityConfigVersionHandler.isVersionIndexEnabled(settings)) { @@ -2474,20 +2458,9 @@ private void tryAddSecurityProvider() { // CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions @Override public void loadExtensions(ExtensionLoader loader) { - if (settings == null - || !settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - )) { - return; - } - // discover & register extensions and their types Set exts = new HashSet<>(loader.loadExtensions(ResourceSharingExtension.class)); - resourcePluginInfo.setResourceSharingExtensions( - exts, - settings.getAsList(ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES) - ); + resourcePluginInfo.setResourceSharingExtensions(exts); // load action-groups in memory ResourceActionGroupsHelper.loadActionGroupsConfig(resourcePluginInfo); diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 4264ba4bfe..9464c815c3 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -79,10 +79,12 @@ import org.opensearch.security.privileges.dlsfls.FieldMasking; import org.opensearch.security.privileges.dlsfls.IndexToRuleMap; import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.resources.ResourcePluginInfo; import org.opensearch.security.resources.ResourceSharingDlsUtils; import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.HeaderHelper; import org.opensearch.security.support.WildcardMatcher; @@ -107,8 +109,8 @@ public class DlsFlsValveImpl implements DlsFlsRequestValve { private final FieldMasking.Config fieldMaskingConfig; private final Settings settings; private final AdminDNs adminDNs; - private boolean isResourceSharingFeatureEnabled = false; - private final WildcardMatcher resourceIndicesMatcher; + private final OpensearchDynamicSetting resourceSharingEnabledSetting; + private final ResourcePluginInfo resourcePluginInfo; public DlsFlsValveImpl( Settings settings, @@ -119,7 +121,8 @@ public DlsFlsValveImpl( ThreadPool threadPool, DlsFlsBaseContext dlsFlsBaseContext, AdminDNs adminDNs, - Set resourceIndices + ResourcePluginInfo resourcePluginInfo, + OpensearchDynamicSetting resourceSharingEnabledSetting ) { super(); this.nodeClient = nodeClient; @@ -132,7 +135,7 @@ public DlsFlsValveImpl( this.dlsFlsBaseContext = dlsFlsBaseContext; this.settings = settings; this.adminDNs = adminDNs; - this.resourceIndicesMatcher = WildcardMatcher.from(resourceIndices); + this.resourcePluginInfo = resourcePluginInfo; clusterService.addListener(event -> { DlsFlsProcessedConfig config = dlsFlsProcessedConfig.get(); @@ -141,10 +144,7 @@ public DlsFlsValveImpl( config.updateClusterStateMetadataAsync(clusterService, threadPool); } }); - this.isResourceSharingFeatureEnabled = settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - ); + this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; } /** @@ -164,26 +164,29 @@ public boolean invoke(PrivilegesEvaluationContext context, final ActionListener< ActionRequest request = context.getRequest(); if (HeaderHelper.isInternalOrPluginRequest(threadContext)) { IndexResolverReplacer.Resolved resolved = context.getResolvedRequest(); - if (isResourceSharingFeatureEnabled - && request instanceof SearchRequest - && resourceIndicesMatcher.matchAll(resolved.getAllIndices())) { - - IndexToRuleMap sharedResourceMap = ResourceSharingDlsUtils.resourceRestrictions( - namedXContentRegistry, - resolved, - userSubject.getUser() - ); + if (resourceSharingEnabledSetting.getDynamicSettingValue() && request instanceof SearchRequest) { - return DlsFilterLevelActionHandler.handle( - context, - sharedResourceMap, - listener, - nodeClient, - clusterService, - OpenSearchSecurityPlugin.GuiceHolder.getIndicesService(), - resolver, - threadContext - ); + Set protectedIndices = resourcePluginInfo.getResourceIndicesForProtectedTypes(); + WildcardMatcher resourceIndicesMatcher = WildcardMatcher.from(protectedIndices); + if (resourceIndicesMatcher.matchAll(resolved.getAllIndices())) { + + IndexToRuleMap sharedResourceMap = ResourceSharingDlsUtils.resourceRestrictions( + namedXContentRegistry, + resolved, + userSubject.getUser() + ); + + return DlsFilterLevelActionHandler.handle( + context, + sharedResourceMap, + listener, + nodeClient, + clusterService, + OpenSearchSecurityPlugin.GuiceHolder.getIndicesService(), + resolver, + threadContext + ); + } } else { return true; } diff --git a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java index 8983a19833..29119a127f 100644 --- a/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/ResourceAccessEvaluator.java @@ -11,7 +11,6 @@ package org.opensearch.security.privileges; import java.util.List; -import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -20,11 +19,11 @@ import org.opensearch.action.DocRequest; import org.opensearch.action.DocWriteRequest; import org.opensearch.action.get.GetRequest; -import org.opensearch.common.settings.Settings; import org.opensearch.core.action.ActionListener; import org.opensearch.core.common.Strings; import org.opensearch.security.resources.ResourceAccessHandler; -import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.resources.ResourcePluginInfo; +import org.opensearch.security.setting.OpensearchDynamicSetting; /** * Evaluates access to resources. The resource plugins must register the indices which hold resource information. @@ -42,14 +41,22 @@ public class ResourceAccessEvaluator { private static final Logger log = LogManager.getLogger(ResourceAccessEvaluator.class); - private final Set resourceIndices; - private final Settings settings; + private final ResourcePluginInfo resourcePluginInfo; private final ResourceAccessHandler resourceAccessHandler; - public ResourceAccessEvaluator(Set resourceIndices, Settings settings, ResourceAccessHandler resourceAccessHandler) { - this.resourceIndices = resourceIndices; - this.settings = settings; + private final OpensearchDynamicSetting resourceSharingEnabledSetting; + private final OpensearchDynamicSetting> protectedResourceTypesSetting; + + public ResourceAccessEvaluator( + ResourcePluginInfo resourcePluginInfo, + ResourceAccessHandler resourceAccessHandler, + final OpensearchDynamicSetting resourceSharingEnabledSetting, + final OpensearchDynamicSetting> protectedResourceTypesSetting + ) { + this.resourcePluginInfo = resourcePluginInfo; this.resourceAccessHandler = resourceAccessHandler; + this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; + this.protectedResourceTypesSetting = protectedResourceTypesSetting; } /** @@ -96,14 +103,9 @@ public void evaluateAsync( * @return true if request should be evaluated, false otherwise */ public boolean shouldEvaluate(ActionRequest request) { - boolean isResourceSharingFeatureEnabled = settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - ); - List protectedTypes = settings.getAsList( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES_DEFAULT - ); + boolean isResourceSharingFeatureEnabled = resourceSharingEnabledSetting.getDynamicSettingValue(); + List protectedTypes = protectedResourceTypesSetting.getDynamicSettingValue(); + if (!isResourceSharingFeatureEnabled) return false; if (!(request instanceof DocRequest docRequest)) return false; /** @@ -128,7 +130,7 @@ public boolean shouldEvaluate(ActionRequest request) { return false; } // if requested index is not a resource sharing index, move on to the regular evaluator - if (!resourceIndices.contains(docRequest.index())) { + if (!resourcePluginInfo.getResourceIndicesForProtectedTypes().contains(docRequest.index())) { log.debug("Request index {} is not a protected resource index", docRequest.index()); return false; } diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java b/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java index ad2be8da0d..65ce3beef1 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessControlClient.java @@ -8,12 +8,14 @@ package org.opensearch.security.resources; +import java.util.List; import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.core.action.ActionListener; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.spi.resources.client.ResourceSharingClient; import org.opensearch.security.spi.resources.sharing.ResourceSharing; import org.opensearch.security.spi.resources.sharing.ShareWith; @@ -28,15 +30,21 @@ public final class ResourceAccessControlClient implements ResourceSharingClient private static final Logger LOGGER = LogManager.getLogger(ResourceAccessControlClient.class); private final ResourceAccessHandler resourceAccessHandler; - private final Set resourceIndices; + private final ResourcePluginInfo resourcePluginInfo; + private final OpensearchDynamicSetting> resourceSharingProtectedResourcesSetting; /** * Constructs a new ResourceAccessControlClient. * */ - public ResourceAccessControlClient(ResourceAccessHandler resourceAccessHandler, Set resourceIndices) { + public ResourceAccessControlClient( + ResourceAccessHandler resourceAccessHandler, + ResourcePluginInfo resourcePluginInfo, + OpensearchDynamicSetting> resourceSharingProtectedResourcesSetting + ) { this.resourceAccessHandler = resourceAccessHandler; - this.resourceIndices = resourceIndices; + this.resourcePluginInfo = resourcePluginInfo; + this.resourceSharingProtectedResourcesSetting = resourceSharingProtectedResourcesSetting; } /** @@ -50,7 +58,7 @@ public ResourceAccessControlClient(ResourceAccessHandler resourceAccessHandler, @Override public void verifyAccess(String resourceId, String resourceIndex, String action, ActionListener listener) { // following situation will arise when resource is onboarded to framework but not marked as protected - if (!resourceIndices.contains(resourceIndex)) { + if (!resourcePluginInfo.getResourceIndicesForProtectedTypes().contains(resourceIndex)) { LOGGER.warn( "Resource '{}' is onboarded to sharing framework but is not marked as protected. Action {} is allowed.", resourceId, @@ -98,4 +106,14 @@ public void revoke(String resourceId, String resourceIndex, ShareWith target, Ac public void getAccessibleResourceIds(String resourceIndex, ActionListener> listener) { resourceAccessHandler.getOwnAndSharedResourceIdsForCurrentUser(resourceIndex, listener); } + + /** + * Returns a flag to indicate whether resource-sharing is enabled for resource-type + * @param resourceType the type for which resource-sharing status is to be checked + * @return true if enabled, false otherwise + */ + @Override + public boolean isFeatureEnabledForType(String resourceType) { + return resourceSharingProtectedResourcesSetting.getDynamicSettingValue().contains(resourceType); + } } diff --git a/src/main/java/org/opensearch/security/resources/ResourceIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceIndexListener.java index 5dab5316c3..0d166d47f0 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceIndexListener.java +++ b/src/main/java/org/opensearch/security/resources/ResourceIndexListener.java @@ -9,6 +9,7 @@ package org.opensearch.security.resources; import java.io.IOException; +import java.util.List; import java.util.Objects; import org.apache.logging.log4j.LogManager; @@ -19,6 +20,7 @@ import org.opensearch.index.engine.Engine; import org.opensearch.index.shard.IndexingOperationListener; import org.opensearch.security.auth.UserSubjectImpl; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.spi.resources.sharing.CreatedBy; import org.opensearch.security.spi.resources.sharing.ResourceSharing; import org.opensearch.security.support.ConfigConstants; @@ -37,10 +39,24 @@ public class ResourceIndexListener implements IndexingOperationListener { private final ResourceSharingIndexHandler resourceSharingIndexHandler; private final ThreadPool threadPool; + private final ResourcePluginInfo resourcePluginInfo; - public ResourceIndexListener(ThreadPool threadPool, Client client, ResourcePluginInfo resourcePluginInfo) { + private final OpensearchDynamicSetting resourceSharingEnabledSetting; + + private final OpensearchDynamicSetting> protectedResourceTypesSetting; + + public ResourceIndexListener( + ThreadPool threadPool, + Client client, + ResourcePluginInfo resourcePluginInfo, + OpensearchDynamicSetting resourceSharingEnabledSetting, + OpensearchDynamicSetting> resourceSharingProtectedResourceTypesSetting + ) { this.threadPool = threadPool; this.resourceSharingIndexHandler = new ResourceSharingIndexHandler(client, threadPool, resourcePluginInfo); + this.resourcePluginInfo = resourcePluginInfo; + this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; + this.protectedResourceTypesSetting = resourceSharingProtectedResourceTypesSetting; } /** @@ -48,7 +64,19 @@ public ResourceIndexListener(ThreadPool threadPool, Client client, ResourcePlugi */ @Override public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult result) { + + if (!resourceSharingEnabledSetting.getDynamicSettingValue()) { + // feature is disabled + return; + } String resourceIndex = shardId.getIndexName(); + + List protectedResourceTypes = protectedResourceTypesSetting.getDynamicSettingValue(); + if (!protectedResourceTypes.contains(resourcePluginInfo.typeByIndex(resourceIndex))) { + // type is marked as not protected + return; + } + log.debug("postIndex called on {}", resourceIndex); String resourceId = index.id(); @@ -104,7 +132,18 @@ public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult re */ @Override public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResult result) { + if (!resourceSharingEnabledSetting.getDynamicSettingValue()) { + // feature is disabled + return; + } String resourceIndex = shardId.getIndexName(); + + List protectedResourceTypes = protectedResourceTypesSetting.getDynamicSettingValue(); + if (!protectedResourceTypes.contains(resourcePluginInfo.typeByIndex(resourceIndex))) { + // type is marked as not protected + return; + } + log.debug("postDelete called on {}", resourceIndex); String resourceId = delete.id(); diff --git a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java index 23c22f0032..ae8ac8cda2 100644 --- a/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java +++ b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; import com.google.common.collect.ImmutableSet; @@ -27,7 +28,9 @@ import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.security.securityconf.FlattenedActionGroups; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.spi.resources.ResourceSharingExtension; +import org.opensearch.security.spi.resources.client.ResourceSharingClient; /** * This class provides information about resource plugins and their associated resource providers and indices. @@ -37,6 +40,10 @@ */ public class ResourcePluginInfo { + private ResourceSharingClient resourceAccessControlClient; + + private OpensearchDynamicSetting> resourceSharingProtectedTypesSetting; + private final Set resourceSharingExtensions = new HashSet<>(); // type <-> index @@ -49,20 +56,26 @@ public class ResourcePluginInfo { // AuthZ: resolved (flattened) groups per type private final Map typeToFlattened = new HashMap<>(); - public void setResourceSharingExtensions(Set extensions, List protectedTypes) { - resourceSharingExtensions.clear(); - typeToIndex.clear(); - indexToType.clear(); - // only assign types if the list setting is non-empty - if (!protectedTypes.isEmpty()) { + // cache current protected types and their indices + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); // make the updates/reads thread-safe + private Set currentProtectedTypes = Collections.emptySet(); // snapshot of last set + private Set cachedProtectedTypeIndices = Collections.emptySet(); // precomputed indices + + public void setResourceSharingProtectedTypesSetting(OpensearchDynamicSetting> resourceSharingProtectedTypesSetting) { + this.resourceSharingProtectedTypesSetting = resourceSharingProtectedTypesSetting; + } + + public void setResourceSharingExtensions(Set extensions) { + lock.writeLock().lock(); + try { + resourceSharingExtensions.clear(); + typeToIndex.clear(); + indexToType.clear(); + // Enforce resource-type unique-ness Set resourceTypes = new HashSet<>(); for (ResourceSharingExtension extension : extensions) { for (var rp : extension.getResourceProviders()) { - // exclude resource types not mentioned in the explicit list. defaults to no resource marked as protected resources - if (!protectedTypes.contains(rp.resourceType())) { - continue; - } if (!resourceTypes.contains(rp.resourceType())) { // add name seen so far to the resource-types set resourceTypes.add(rp.resourceType()); @@ -80,16 +93,64 @@ public void setResourceSharingExtensions(Set extension } } } + resourceSharingExtensions.addAll(extensions); + + // Whenever providers change, invalidate protected caches so next update refreshes them + currentProtectedTypes = Collections.emptySet(); + cachedProtectedTypeIndices = Collections.emptySet(); + } finally { + lock.writeLock().unlock(); + } + } + + public void updateProtectedTypes(List protectedTypes) { + lock.writeLock().lock(); + try { + // Rebuild mappings based on the current allowlist + typeToIndex.clear(); + indexToType.clear(); + + if (protectedTypes == null || protectedTypes.isEmpty()) { + // No protected types -> leave maps empty + currentProtectedTypes = Collections.emptySet(); + cachedProtectedTypeIndices = Collections.emptySet(); + return; + } + + // Cache current protected set as an unmodifiable snapshot + currentProtectedTypes = Collections.unmodifiableSet(new LinkedHashSet<>(protectedTypes)); + + for (ResourceSharingExtension extension : resourceSharingExtensions) { + for (var rp : extension.getResourceProviders()) { + final String type = rp.resourceType(); + if (!currentProtectedTypes.contains(type)) continue; + + final String index = rp.resourceIndexName(); + typeToIndex.put(type, index); + indexToType.put(index, type); + } + } + + // pre-compute indices for current protected set + cachedProtectedTypeIndices = Collections.unmodifiableSet(new LinkedHashSet<>(typeToIndex.values())); + } finally { + lock.writeLock().unlock(); } - resourceSharingExtensions.addAll(extensions); } public Set getResourceSharingExtensions() { return ImmutableSet.copyOf(resourceSharingExtensions); } - /** Register/merge action-group names for a given resource type. */ + public void setResourceSharingClient(ResourceSharingClient resourceAccessControlClient) { + this.resourceAccessControlClient = resourceAccessControlClient; + } + public ResourceSharingClient getResourceAccessControlClient() { + return resourceAccessControlClient; + } + + /** Register/merge action-group names for a given resource type. */ public record ResourceDashboardInfo(String resourceType, Set actionGroups // names only (for UI) ) implements ToXContentObject { @Override @@ -103,36 +164,96 @@ public XContentBuilder toXContent(XContentBuilder b, Params p) throws IOExceptio public void registerActionGroupNames(String resourceType, Collection names) { if (resourceType == null || names == null) return; - typeToGroupNames.computeIfAbsent(resourceType, k -> new LinkedHashSet<>()) - .addAll(names.stream().filter(Objects::nonNull).map(String::trim).filter(s -> !s.isEmpty()).toList()); + lock.writeLock().lock(); + try { + typeToGroupNames.computeIfAbsent(resourceType, k -> new LinkedHashSet<>()) + .addAll(names.stream().filter(Objects::nonNull).map(String::trim).filter(s -> !s.isEmpty()).toList()); + } finally { + lock.writeLock().unlock(); + } } public void registerFlattened(String resourceType, FlattenedActionGroups flattened) { if (resourceType == null || flattened == null) return; - typeToFlattened.put(resourceType, flattened); + lock.writeLock().lock(); + try { + typeToFlattened.put(resourceType, flattened); + } finally { + lock.writeLock().unlock(); + } } public FlattenedActionGroups flattenedForType(String resourceType) { - return typeToFlattened.getOrDefault(resourceType, FlattenedActionGroups.EMPTY); + lock.readLock().lock(); + try { + return typeToFlattened.getOrDefault(resourceType, FlattenedActionGroups.EMPTY); + } finally { + lock.readLock().unlock(); + } } public String typeByIndex(String index) { - return indexToType.get(index); + lock.readLock().lock(); + try { + return indexToType.get(index); + } finally { + lock.readLock().unlock(); + } } public String indexByType(String type) { - return typeToIndex.get(type); + lock.readLock().lock(); + try { + return typeToIndex.get(type); + } finally { + lock.readLock().unlock(); + } } public Set getResourceTypes() { - return typeToIndex.keySet() - .stream() - .map(s -> new ResourceDashboardInfo(s, Collections.unmodifiableSet(typeToGroupNames.getOrDefault(s, new LinkedHashSet<>())))) - .collect(Collectors.toCollection(LinkedHashSet::new)); + lock.readLock().lock(); + try { + return typeToIndex.keySet() + .stream() + .map( + s -> new ResourceDashboardInfo(s, Collections.unmodifiableSet(typeToGroupNames.getOrDefault(s, new LinkedHashSet<>()))) + ) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } finally { + lock.readLock().unlock(); + } } public Set getResourceIndices() { - return indexToType.keySet(); + lock.readLock().lock(); + try { + return new LinkedHashSet<>(indexToType.keySet()); + } finally { + lock.readLock().unlock(); + } + } + + public Set getResourceIndicesForProtectedTypes() { + List resourceTypes = this.resourceSharingProtectedTypesSetting.getDynamicSettingValue(); + if (resourceTypes == null || resourceTypes.isEmpty()) { + return Collections.emptySet(); + } + + lock.readLock().lock(); + try { + // If caller is asking for the current protected set, return the cache + if (new LinkedHashSet<>(resourceTypes).equals(currentProtectedTypes)) { + return cachedProtectedTypeIndices; + } + + return indexToType.entrySet() + .stream() + .filter(e -> resourceTypes.contains(e.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } finally { + lock.readLock().unlock(); + } } } diff --git a/src/main/java/org/opensearch/security/resources/api/list/AccessibleResourcesRestAction.java b/src/main/java/org/opensearch/security/resources/api/list/AccessibleResourcesRestAction.java index cef53f82b9..d9c6975dfc 100644 --- a/src/main/java/org/opensearch/security/resources/api/list/AccessibleResourcesRestAction.java +++ b/src/main/java/org/opensearch/security/resources/api/list/AccessibleResourcesRestAction.java @@ -29,6 +29,7 @@ import org.opensearch.security.resources.ResourceAccessHandler; import org.opensearch.security.resources.ResourcePluginInfo; import org.opensearch.security.resources.SharingRecord; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.transport.client.node.NodeClient; import static org.opensearch.rest.RestRequest.Method.GET; @@ -44,11 +45,17 @@ public class AccessibleResourcesRestAction extends BaseRestHandler { private final ResourceAccessHandler resourceAccessHandler; private final ResourcePluginInfo resourcePluginInfo; + private final OpensearchDynamicSetting resourceSharingEnabledSetting; - public AccessibleResourcesRestAction(final ResourceAccessHandler resourceAccessHandler, ResourcePluginInfo resourcePluginInfo) { + public AccessibleResourcesRestAction( + final ResourceAccessHandler resourceAccessHandler, + ResourcePluginInfo resourcePluginInfo, + OpensearchDynamicSetting resourceSharingEnabledSetting + ) { super(); this.resourceAccessHandler = resourceAccessHandler; this.resourcePluginInfo = resourcePluginInfo; + this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; } @Override @@ -63,6 +70,9 @@ public String getName() { @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + if (!resourceSharingEnabledSetting.getDynamicSettingValue()) { + return channel -> { channel.sendResponse(new BytesRestResponse(RestStatus.NOT_IMPLEMENTED, "Feature disabled.")); }; + } final String resourceType = Objects.requireNonNull(request.param("resource_type"), "resource_type is required"); final String resourceIndex = resourcePluginInfo.indexByType(resourceType); diff --git a/src/main/java/org/opensearch/security/resources/api/list/ResourceTypesRestAction.java b/src/main/java/org/opensearch/security/resources/api/list/ResourceTypesRestAction.java index 733c36eb4d..6a5c0f4d1f 100644 --- a/src/main/java/org/opensearch/security/resources/api/list/ResourceTypesRestAction.java +++ b/src/main/java/org/opensearch/security/resources/api/list/ResourceTypesRestAction.java @@ -10,7 +10,6 @@ import java.io.IOException; import java.util.List; -import java.util.Set; import com.google.common.collect.ImmutableList; import org.apache.logging.log4j.LogManager; @@ -25,6 +24,7 @@ import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; import org.opensearch.security.resources.ResourcePluginInfo; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.transport.client.node.NodeClient; import static org.opensearch.rest.RestRequest.Method.GET; @@ -38,11 +38,17 @@ public class ResourceTypesRestAction extends BaseRestHandler { private static final Logger LOGGER = LogManager.getLogger(ResourceTypesRestAction.class); - private final Set resourceTypes; + private final ResourcePluginInfo resourcePluginInfo; - public ResourceTypesRestAction(final ResourcePluginInfo resourcePluginInfo) { + private final OpensearchDynamicSetting resourceSharingEnabledSetting; + + public ResourceTypesRestAction( + final ResourcePluginInfo resourcePluginInfo, + OpensearchDynamicSetting resourceSharingEnabledSetting + ) { super(); - this.resourceTypes = resourcePluginInfo.getResourceTypes(); + this.resourcePluginInfo = resourcePluginInfo; + this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; } @Override @@ -57,11 +63,14 @@ public String getName() { @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + if (!resourceSharingEnabledSetting.getDynamicSettingValue()) { + return channel -> { channel.sendResponse(new BytesRestResponse(RestStatus.NOT_IMPLEMENTED, "Feature disabled.")); }; + } return channel -> { try (XContentBuilder builder = channel.newBuilder()) { // NOSONAR builder.startObject(); builder.startArray("types"); - for (var p : resourceTypes) { + for (var p : resourcePluginInfo.getResourceTypes()) { p.toXContent(builder, ToXContent.EMPTY_PARAMS); } builder.endArray(); diff --git a/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java b/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java index 8cc9d87032..16a37afd6f 100644 --- a/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java +++ b/src/main/java/org/opensearch/security/resources/api/share/ShareRestAction.java @@ -15,21 +15,18 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.OpenSearchStatusException; -import org.opensearch.core.action.ActionListener; import org.opensearch.core.rest.RestStatus; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; import org.opensearch.security.resources.ResourcePluginInfo; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.transport.client.node.NodeClient; import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.rest.RestRequest.Method.PATCH; import static org.opensearch.rest.RestRequest.Method.PUT; -import static org.opensearch.security.dlic.rest.api.Responses.ok; -import static org.opensearch.security.dlic.rest.api.Responses.response; import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_API_RESOURCE_ROUTE_PREFIX; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; @@ -42,9 +39,17 @@ public class ShareRestAction extends BaseRestHandler { private static final Logger LOGGER = LogManager.getLogger(ShareRestAction.class); private final ResourcePluginInfo resourcePluginInfo; - - public ShareRestAction(ResourcePluginInfo resourcePluginInfo) { + private final OpensearchDynamicSetting resourceSharingEnabledSetting; + private final OpensearchDynamicSetting> resourceSharingProtectedTypesSetting; + + public ShareRestAction( + ResourcePluginInfo resourcePluginInfo, + OpensearchDynamicSetting resourceSharingEnabledSetting, + OpensearchDynamicSetting> resourceSharingProtectedTypesSetting + ) { this.resourcePluginInfo = resourcePluginInfo; + this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; + this.resourceSharingProtectedTypesSetting = resourceSharingProtectedTypesSetting; } @Override @@ -62,6 +67,10 @@ public String getName() { @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + if (!resourceSharingEnabledSetting.getDynamicSettingValue()) { + return channel -> { channel.sendResponse(new BytesRestResponse(RestStatus.NOT_IMPLEMENTED, "Feature disabled.")); }; + } + // These two params will only be present with GET request String resourceId = request.param("resource_id"); String resourceType = request.param("resource_type"); @@ -85,22 +94,14 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli ShareRequest shareRequest = builder.build(); - return channel -> { - client.executeLocally( - ShareAction.INSTANCE, - shareRequest, - ActionListener.wrap(resp -> ok(channel, resp::toXContent), e -> handleError(channel, e)) - ); - }; - } - - private void handleError(RestChannel channel, Exception e) { - LOGGER.error("Error while processing request", e); - String message = e.getMessage(); - if (e instanceof OpenSearchStatusException ex) { - response(channel, ex.status(), message); - } else { - channel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, message)); + if (shareRequest.type() != null && !resourceSharingProtectedTypesSetting.getDynamicSettingValue().contains(shareRequest.type())) { + return channel -> { + channel.sendResponse( + new BytesRestResponse(RestStatus.BAD_REQUEST, "Resource type " + resourceType + " is not marked as protected.") + ); + }; } + + return channel -> { client.executeLocally(ShareAction.INSTANCE, shareRequest, new RestToXContentListener<>(channel)); }; } } diff --git a/src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java b/src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java new file mode 100644 index 0000000000..786fed8e40 --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/settings/ResourceSharingFeatureFlagSetting.java @@ -0,0 +1,63 @@ +/* + * 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.resources.settings; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.settings.ClusterSettings; +import org.opensearch.common.settings.Setting; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.resources.ResourcePluginInfo; +import org.opensearch.security.setting.OpensearchDynamicSetting; +import org.opensearch.security.spi.resources.client.ResourceSharingClient; +import org.opensearch.security.support.ConfigConstants; + +// CS-SUPPRESS-SINGLE: RegexpSingleline get Resource Sharing Extensions +public class ResourceSharingFeatureFlagSetting extends OpensearchDynamicSetting { + private static final Logger logger = LogManager.getLogger(ResourceSharingFeatureFlagSetting.class); + + public static final Setting RESOURCE_SHARING_ENABLED = Setting.boolSetting( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT, + Setting.Property.NodeScope, + Setting.Property.Dynamic + ); + + private final ResourcePluginInfo resourcePluginInfo; + + public ResourceSharingFeatureFlagSetting(final Settings settings, final ResourcePluginInfo resourcePluginInfo) { + super(RESOURCE_SHARING_ENABLED, RESOURCE_SHARING_ENABLED.get(settings)); + this.resourcePluginInfo = resourcePluginInfo; + } + + @Override + public void registerClusterSettingsChangeListener(final ClusterSettings clusterSettings) { + clusterSettings.addSettingsUpdateConsumer(RESOURCE_SHARING_ENABLED, isEnabled -> { + logger.info(getClusterChangeMessage(isEnabled)); + setDynamicSettingValue(isEnabled); + + if (isEnabled) { + ResourceSharingClient client = resourcePluginInfo.getResourceAccessControlClient(); + resourcePluginInfo.getResourceSharingExtensions().forEach(ext -> ext.assignResourceSharingClient(client)); + } else { + resourcePluginInfo.getResourceSharingExtensions().forEach(ext -> ext.assignResourceSharingClient(null)); + } + }); + } + + @Override + protected String getClusterChangeMessage(final Boolean isEnabled) { + return String.format( + "Detected change in settings, cluster setting for resource-sharing feature flag is %s", + isEnabled ? "enabled" : "disabled" + ); + } +} +// CS-ENFORCE-SINGLE diff --git a/src/main/java/org/opensearch/security/resources/settings/ResourceSharingProtectedResourcesSetting.java b/src/main/java/org/opensearch/security/resources/settings/ResourceSharingProtectedResourcesSetting.java new file mode 100644 index 0000000000..3afe137432 --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/settings/ResourceSharingProtectedResourcesSetting.java @@ -0,0 +1,55 @@ +/* + * 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.resources.settings; + +import java.util.List; +import java.util.function.Function; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.settings.ClusterSettings; +import org.opensearch.common.settings.Setting; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.resources.ResourcePluginInfo; +import org.opensearch.security.setting.OpensearchDynamicSetting; +import org.opensearch.security.support.ConfigConstants; + +public class ResourceSharingProtectedResourcesSetting extends OpensearchDynamicSetting> { + private static final Logger logger = LogManager.getLogger(ResourceSharingProtectedResourcesSetting.class); + + public static final Setting> PROTECTED_TYPES = Setting.listSetting( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_PROTECTED_TYPES_DEFAULT, + Function.identity(), + Setting.Property.NodeScope, + Setting.Property.Dynamic + ); + + private final ResourcePluginInfo resourcePluginInfo; + + public ResourceSharingProtectedResourcesSetting(final Settings settings, final ResourcePluginInfo resourcePluginInfo) { + super(PROTECTED_TYPES, PROTECTED_TYPES.get(settings)); + this.resourcePluginInfo = resourcePluginInfo; + } + + @Override + public void registerClusterSettingsChangeListener(final ClusterSettings clusterSettings) { + clusterSettings.addSettingsUpdateConsumer(PROTECTED_TYPES, newValue -> { + logger.info(getClusterChangeMessage(newValue)); + setDynamicSettingValue(newValue); + this.resourcePluginInfo.updateProtectedTypes(newValue); + }); + } + + @Override + protected String getClusterChangeMessage(final List newValue) { + return String.format("Detected change in settings, new resource-sharing protected resource-types are %s", newValue); + } +} diff --git a/src/main/java/org/opensearch/security/resources/settings/package-info.java b/src/main/java/org/opensearch/security/resources/settings/package-info.java new file mode 100644 index 0000000000..896f465ca2 --- /dev/null +++ b/src/main/java/org/opensearch/security/resources/settings/package-info.java @@ -0,0 +1,12 @@ +/* + * 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. + */ + +/** + * Contains settings related to resource-sharing feature + */ +package org.opensearch.security.resources.settings; diff --git a/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java b/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java index 9203f4e92c..7353633071 100644 --- a/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java @@ -33,16 +33,15 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.rest.RestStatus; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; @@ -82,7 +81,7 @@ public class DashboardsInfoAction extends BaseRestHandler { private final PrivilegesEvaluator evaluator; private final ThreadContext threadContext; - private final boolean isResourceSharingFeatureEnabled; + private final OpensearchDynamicSetting resourceSharingEnabledSetting; public static final String DEFAULT_PASSWORD_MESSAGE = "Password should be at least 8 characters long and contain at least one " + "uppercase letter, one lowercase letter, one digit, and one special character."; @@ -90,16 +89,12 @@ public class DashboardsInfoAction extends BaseRestHandler { public static final String DEFAULT_PASSWORD_REGEX = "(?=.*[A-Z])(?=.*[^a-zA-Z\\d])(?=.*[0-9])(?=.*[a-z]).{8,}"; public DashboardsInfoAction( - final Settings settings, - final RestController controller, final PrivilegesEvaluator evaluator, - final ThreadPool threadPool + final ThreadPool threadPool, + OpensearchDynamicSetting resourceSharingEnabledSetting ) { super(); - isResourceSharingFeatureEnabled = settings.getAsBoolean( - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, - ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT - ); + this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; this.threadContext = threadPool.getThreadContext(); this.evaluator = evaluator; } @@ -145,7 +140,7 @@ public void accept(RestChannel channel) throws Exception { "password_validation_regex", client.settings().get(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, DEFAULT_PASSWORD_REGEX) ); - builder.field("resource_sharing_enabled", isResourceSharingFeatureEnabled); + builder.field("resource_sharing_enabled", resourceSharingEnabledSetting.getDynamicSettingValue()); builder.endObject(); response = new BytesRestResponse(RestStatus.OK, builder); diff --git a/src/main/java/org/opensearch/security/setting/OpensearchDynamicSetting.java b/src/main/java/org/opensearch/security/setting/OpensearchDynamicSetting.java index 7c84cf779d..81e0657fb1 100644 --- a/src/main/java/org/opensearch/security/setting/OpensearchDynamicSetting.java +++ b/src/main/java/org/opensearch/security/setting/OpensearchDynamicSetting.java @@ -45,7 +45,7 @@ protected String getClusterChangeMessage(final T dynamicSettingNewValue) { return String.format("Detected change in settings, updated cluster setting value is %s", dynamicSettingNewValue); } - private void setDynamicSettingValue(final T dynamicSettingValue) { + protected void setDynamicSettingValue(final T dynamicSettingValue) { this.dynamicSettingValue = dynamicSettingValue; } diff --git a/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java index c82f2f9823..d23c9a29cb 100644 --- a/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/ResourceAccessEvaluatorTest.java @@ -8,8 +8,6 @@ package org.opensearch.security.privileges; -import java.util.Collections; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -20,6 +18,8 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.security.auth.UserSubjectImpl; import org.opensearch.security.resources.ResourceAccessHandler; +import org.opensearch.security.resources.ResourcePluginInfo; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.support.ConfigConstants; import org.mockito.ArgumentCaptor; @@ -40,6 +40,8 @@ public class ResourceAccessEvaluatorTest { @Mock private ResourceAccessHandler resourceAccessHandler; + @Mock + private ResourcePluginInfo resourcePluginInfo; @Mock private PrivilegesEvaluationContext context; @@ -51,9 +53,13 @@ public class ResourceAccessEvaluatorTest { @Before public void setup() { - Settings settings = Settings.builder().put(ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, true).build(); threadContext = new ThreadContext(Settings.EMPTY); - evaluator = new ResourceAccessEvaluator(Collections.singleton(IDX), settings, resourceAccessHandler); + evaluator = new ResourceAccessEvaluator( + resourcePluginInfo, + resourceAccessHandler, + mock(OpensearchDynamicSetting.class), + mock(OpensearchDynamicSetting.class) + ); } private void stubAuthenticatedUser() {