Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ebfd5bd
Makes resources settings dynamically updateable
DarshitChanpura Oct 2, 2025
32f903b
Allows plugin to be control codepath based on protected resource types
DarshitChanpura Oct 2, 2025
5f7beb4
Fix setting registration
DarshitChanpura Oct 2, 2025
696482e
Merge remote-tracking branch 'upstream/main' into dynamic-resource-se…
DarshitChanpura Oct 2, 2025
c2988ea
Adds changelog entry
DarshitChanpura Oct 2, 2025
fbdae9f
Corrects usage of resource-sharing flag
DarshitChanpura Oct 2, 2025
a3493a6
Fix share action rest channel consumer
DarshitChanpura Oct 7, 2025
9dd02a1
Fix sample plugin tests
DarshitChanpura Oct 7, 2025
9930ae3
Merge remote-tracking branch 'upstream/main' into dynamic-resource-se…
DarshitChanpura Oct 7, 2025
1a94f40
Merge remote-tracking branch 'upstream/main' into dynamic-resource-se…
DarshitChanpura Oct 7, 2025
42b7230
Merge remote-tracking branch 'upstream/main' into dynamic-resource-se…
DarshitChanpura Oct 8, 2025
b333d12
Registers extensions regardless of whether their resources are marked…
DarshitChanpura Oct 8, 2025
ea333b9
Adds tests for the new dynamic settings and update existing tests to …
DarshitChanpura Oct 8, 2025
7c2c896
Adds documentation
DarshitChanpura Oct 8, 2025
258229d
Merge branch 'main' into dynamic-resource-settings
DarshitChanpura Oct 9, 2025
13204e2
Merge remote-tracking branch 'upstream/main' into dynamic-resource-se…
DarshitChanpura Oct 9, 2025
09b16eb
Merge remote-tracking branch 'upstream/main' into dynamic-resource-se…
DarshitChanpura Oct 13, 2025
8d186fe
Fixes resource search request evaluation
DarshitChanpura Oct 13, 2025
b707959
Adds caching to resource indices and makes read-write operations thre…
DarshitChanpura Oct 13, 2025
877bb11
Fixes type enabled check
DarshitChanpura Oct 13, 2025
f9fb61a
Refactors resource type fetch logic
DarshitChanpura Oct 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
54 changes: 53 additions & 1 deletion RESOURCE_SHARING_AND_ACCESS_CONTROL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: [<type-1>, <type-2>]`.

---

Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -109,25 +111,26 @@ 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);

}
}

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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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)
);

Expand All @@ -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";
Expand Down Expand Up @@ -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 """
{
Expand All @@ -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 """
{
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -335,7 +304,6 @@ public void wipeOutResourceEntries() {
String jsonBody = "{ \"query\": { \"match_all\": {} } }";
TestRestClient.HttpResponse resp = client.postJson(endpoint, jsonBody);
resp.assertStatusCode(HttpStatus.SC_OK);

}
}

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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);
}
}
Expand Down
Loading
Loading