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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- Register cluster settings listener for `plugins.security.cache.ttl_minutes` ([#5324](https://github.com/opensearch-project/security/pull/5324))

### Changed
- Use extendedPlugins in integrationTest framework for sample resource plugin testing ([#5322](https://github.com/opensearch-project/security/pull/5322))

### Dependencies
- Bump `guava_version` from 33.4.6-jre to 33.4.8-jre ([#5284](https://github.com/opensearch-project/security/pull/5284))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

package org.opensearch.sample;

import java.util.List;
import java.util.Map;

import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
Expand All @@ -19,8 +20,10 @@
import org.junit.Test;
import org.junit.runner.RunWith;

import org.opensearch.Version;
import org.opensearch.painless.PainlessModulePlugin;
import org.opensearch.sample.resource.client.ResourceSharingClientAccessor;
import org.opensearch.plugins.PluginInfo;
import org.opensearch.security.OpenSearchSecurityPlugin;
import org.opensearch.security.resources.ResourcePluginInfo;
import org.opensearch.security.spi.resources.ResourceAccessActionGroups;
import org.opensearch.security.spi.resources.ResourceSharingExtension;
Expand All @@ -39,7 +42,6 @@
import static org.opensearch.sample.SampleResourcePluginTestHelper.SAMPLE_RESOURCE_SHARE_ENDPOINT;
import static org.opensearch.sample.SampleResourcePluginTestHelper.SAMPLE_RESOURCE_UPDATE_ENDPOINT;
import static org.opensearch.sample.SampleResourcePluginTestHelper.SHARED_WITH_USER;
import static org.opensearch.sample.SampleResourcePluginTestHelper.createResourceAccessControlClient;
import static org.opensearch.sample.SampleResourcePluginTestHelper.revokeAccessPayload;
import static org.opensearch.sample.SampleResourcePluginTestHelper.shareWithPayload;
import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME;
Expand All @@ -61,7 +63,20 @@ public class SampleResourcePluginTests {

@ClassRule
public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE)
.plugin(SampleResourcePlugin.class, PainlessModulePlugin.class)
.plugin(PainlessModulePlugin.class)
.plugin(
new PluginInfo(
SampleResourcePlugin.class.getName(),
"classpath plugin",
"NA",
Version.CURRENT,
"1.8",
SampleResourcePlugin.class.getName(),
null,
List.of(OpenSearchSecurityPlugin.class.getName()),
false
)
)
.anonymousAuth(true)
.authc(AUTHC_HTTPBASIC_INTERNAL)
.users(USER_ADMIN, SHARED_WITH_USER)
Expand Down Expand Up @@ -95,7 +110,6 @@ public void testPluginInstalledCorrectly() {
@Test
public void testCreateUpdateDeleteSampleResource() throws Exception {
String resourceId;
String resourceSharingDocId;
// create sample resource
try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) {
String sampleResource = """
Expand All @@ -106,35 +120,13 @@ public void testCreateUpdateDeleteSampleResource() throws Exception {
response.assertStatusCode(HttpStatus.SC_OK);

resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim();
}

// Create an entry in resource-sharing index
try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) {
// Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually
String json = """
{
"source_idx": ".sample_resource_sharing_plugin",
"resource_id": "%s",
"created_by": {
"user": "admin"
}
}
""".formatted(resourceId);

TestRestClient.HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json);
assertThat(response.getStatusReason(), containsString("Created"));
resourceSharingDocId = response.bodyAsJsonNode().get("_id").asText();
resourcePluginInfo.getResourceSharingExtensionsMutable().add(resourceSharingExtension);

ResourceSharingClientAccessor.getInstance().setResourceSharingClient(createResourceAccessControlClient(cluster));

Awaitility.await()
.alias("Wait until resource data is populated")
.until(() -> client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId).getStatusCode(), equalTo(200));
response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId);
response.assertStatusCode(HttpStatus.SC_OK);
assertThat(response.getBody(), containsString("sample"));
// Wait until resource-sharing entry is successfully created
}

try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) {
Awaitility.await()
.alias("Wait until resource-sharing data is populated")
.until(
Expand All @@ -154,16 +146,7 @@ public void testCreateUpdateDeleteSampleResource() throws Exception {
sampleResourceUpdated
);
updateResponse.assertStatusCode(HttpStatus.SC_OK);
}

// resource should be visible to super-admin
try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) {
Awaitility.await()
.alias("Wait until resource-sharing data is populated")
.until(
() -> client.get(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_search").bodyAsJsonNode().get("hits").get("hits").size(),
equalTo(1)
);
TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId);
response.assertStatusCode(HttpStatus.SC_OK);
assertThat(response.getBody(), containsString("sampleUpdated"));
Expand Down Expand Up @@ -194,15 +177,6 @@ public void testCreateUpdateDeleteSampleResource() throws Exception {
);
}

try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) {
Awaitility.await()
.alias("Wait until resource-sharing data is populated")
.until(
() -> client.get(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_search").bodyAsJsonNode().get("hits").get("hits").size(),
equalTo(1)
);
}

// share resource with shared_with user
try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) {
TestRestClient.HttpResponse response = client.postJson(
Expand All @@ -227,13 +201,6 @@ public void testCreateUpdateDeleteSampleResource() throws Exception {
assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1));
}

// resource is still visible to super-admin
try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) {
TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId);
response.assertStatusCode(HttpStatus.SC_OK);
assertThat(response.getBody(), containsString("sampleUpdated"));
}

// revoke share_with_user's access
try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) {
TestRestClient.HttpResponse response = client.postJson(
Expand Down Expand Up @@ -266,30 +233,18 @@ public void testCreateUpdateDeleteSampleResource() throws Exception {
response.assertStatusCode(HttpStatus.SC_OK);
}

// corresponding entry should be removed from resource-sharing index
try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) {
// Since test framework doesn't yet allow loading ex tensions we need to delete the resource sharing entry manually
TestRestClient.HttpResponse response = client.delete(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc/" + resourceSharingDocId);
response.assertStatusCode(HttpStatus.SC_OK);

Awaitility.await()
.alias("Wait until resource-sharing data is updated")
.until(
() -> client.get(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_search").bodyAsJsonNode().get("hits").get("hits").size(),
equalTo(0)
);
}

// get sample resource with SHARED_WITH_USER
try (TestRestClient client = cluster.getRestClient(SHARED_WITH_USER)) {
TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId);
response.assertStatusCode(HttpStatus.SC_NOT_FOUND);
Awaitility.await()
.alias("Wait until resource-sharing data is deleted")
.until(() -> client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId).getStatusCode(), equalTo(HttpStatus.SC_NOT_FOUND));
}

// get sample resource with admin
try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) {
TestRestClient.HttpResponse response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId);
response.assertStatusCode(HttpStatus.SC_NOT_FOUND);
Awaitility.await()
.alias("Wait until resource-sharing data is deleted")
.until(() -> client.get(SAMPLE_RESOURCE_GET_ENDPOINT + "/" + resourceId).getStatusCode(), equalTo(HttpStatus.SC_NOT_FOUND));
}
}

Expand All @@ -310,34 +265,13 @@ public void testDirectAccess() throws Exception {
resourceId = response.getTextFromJsonBody("/message").split(":")[1].trim();
}

// Create an entry in resource-sharing index
try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) {
// Since test framework doesn't yet allow loading ex tensions we need to create a resource sharing entry manually
String json = """
{
"source_idx": "%s",
"resource_id": "%s",
"created_by": {
"user": "admin"
}
}
""".formatted(RESOURCE_INDEX_NAME, resourceId);
HttpResponse response = client.postJson(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_doc", json);
assertThat(response.getStatusReason(), containsString("Created"));
resourcePluginInfo.getResourceSharingExtensionsMutable().add(resourceSharingExtension);

ResourceSharingClientAccessor.getInstance().setResourceSharingClient(createResourceAccessControlClient(cluster));

Awaitility.await()
.alias("Wait until resource-sharing data is populated")
.until(
() -> client.get(OPENSEARCH_RESOURCE_SHARING_INDEX + "/_search").bodyAsJsonNode().get("hits").get("hits").size(),
equalTo(1)
);
response = client.get(SAMPLE_RESOURCE_GET_ENDPOINT);
response.assertStatusCode(HttpStatus.SC_OK);
assertThat(response.bodyAsJsonNode().get("resources").size(), equalTo(1));
assertThat(response.getBody(), containsString("sample"));
}

// admin should not be able to access resource directly since system index protection is enabled, but can access via sample plugin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@
*/
public class ResourceSharing implements ToXContentFragment, NamedWriteable {

/**
* The unique identifier of the resource sharing entry
*
* TODO If this moves to a shadow index for each resource index, then use the resourceId as the key for both
*/
private String docId;

/**
* The index where the resource is defined
*/
Expand Down Expand Up @@ -63,6 +70,14 @@
this.shareWith = shareWith;
}

public String getDocId() {
return docId;
}

public void setDocId(String docId) {
this.docId = docId;
}

public String getSourceIdx() {
return sourceIdx;
}
Expand Down Expand Up @@ -95,6 +110,15 @@
this.shareWith = shareWith;
}

public void share(String accessLevel, SharedWithActionGroup target) {
if (shareWith == null) {
shareWith = new ShareWith(Set.of(target));
} else {
SharedWithActionGroup sharedWith = shareWith.atAccessLevel(accessLevel);
sharedWith.share(target);

Check warning on line 118 in spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java

View check run for this annotation

Codecov / codecov/patch

spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java#L117-L118

Added lines #L117 - L118 were not covered by tests
}
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

import org.opensearch.core.common.io.stream.NamedWriteable;
import org.opensearch.core.common.io.stream.StreamInput;
Expand Down Expand Up @@ -57,6 +58,14 @@ public Set<SharedWithActionGroup> getSharedWithActionGroups() {
return sharedWithActionGroups;
}

public Set<String> accessLevels() {
return sharedWithActionGroups.stream().map(SharedWithActionGroup::getActionGroup).collect(Collectors.toSet());
}

public SharedWithActionGroup atAccessLevel(String accessLevel) {
return sharedWithActionGroups.stream().filter(g -> accessLevel.equals(g.getActionGroup())).findFirst().orElse(null);
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@
return actionGroupRecipients;
}

public void share(SharedWithActionGroup target) {
Map<Recipient, Set<String>> targetRecipients = target.actionGroupRecipients.getRecipients();

Check warning on line 61 in spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithActionGroup.java

View check run for this annotation

Codecov / codecov/patch

spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithActionGroup.java#L61

Added line #L61 was not covered by tests
for (Recipient recipientType : targetRecipients.keySet()) {
Set<String> recipients = actionGroupRecipients.getRecipientsByType(recipientType);
recipients.addAll(targetRecipients.get(recipientType));
}
}

Check warning on line 66 in spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithActionGroup.java

View check run for this annotation

Codecov / codecov/patch

spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithActionGroup.java#L63-L66

Added lines #L63 - L66 were not covered by tests

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.field(actionGroup);
Expand Down Expand Up @@ -117,6 +125,10 @@
return recipients;
}

public Set<String> getRecipientsByType(Recipient recipientType) {
return recipients.computeIfAbsent(recipientType, key -> new HashSet<>());

Check warning on line 129 in spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithActionGroup.java

View check run for this annotation

Codecov / codecov/patch

spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithActionGroup.java#L129

Added line #L129 was not covered by tests
}

@Override
public String getWriteableName() {
return "action_group_recipients";
Expand Down
Loading
Loading