Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3f53268
Make resource sharing records workspace-aware (spike)
DarshitChanpura Aug 4, 2026
e3467f3
Inherit resource access from workspaces on the write path
DarshitChanpura Aug 4, 2026
4672922
Guard workspace access inheritance against container cycles
DarshitChanpura Aug 5, 2026
00bd5d8
Drop version guard on workspaces serialization
DarshitChanpura Aug 6, 2026
76670dd
Batch workspace lookups on the access-check hot path
DarshitChanpura Aug 6, 2026
7412d76
Backfill workspace membership during migration
DarshitChanpura Aug 8, 2026
d71f36d
Merge remote-tracking branch 'upstream/main' into workspace-aware-sha…
DarshitChanpura Aug 8, 2026
bcc3024
Address Code-Diff-Analyzer findings on workspace sharing
DarshitChanpura Aug 8, 2026
0a7486f
Merge remote-tracking branch 'upstream/main' into workspace-aware-sha…
DarshitChanpura Aug 25, 2026
5333eba
Make ResourceSharing round-trip over the transport wire
DarshitChanpura Aug 25, 2026
19af1df
Backfill workspaces onto already-migrated sharing records
DarshitChanpura Aug 25, 2026
3115d4c
Integration-test workspace backfill during migration
DarshitChanpura Aug 25, 2026
48ccf99
Integration-test workspace-aware live indexing
DarshitChanpura Aug 26, 2026
533c69a
Add SPI seam for trusted workspace-membership resolution
DarshitChanpura Aug 26, 2026
31fde47
Unit-test workspace index-handler + extraction paths
DarshitChanpura Aug 26, 2026
114fdc4
Filter workspace visibility on the resource workspaces field
DarshitChanpura Sep 8, 2026
8db592c
Merge remote-tracking branch 'upstream/main' into workspace-aware-sha…
DarshitChanpura Sep 8, 2026
a1dc646
Filter DLS on the provider-declared workspaces field
DarshitChanpura Sep 9, 2026
e2f4225
Simplify workspace access checks; address review nits
DarshitChanpura Sep 9, 2026
d86d9c6
Make workspace-sharing comments crisp and self-contained
DarshitChanpura Sep 9, 2026
725dfb3
Keep sharing records in sync with resource workspaces
DarshitChanpura Sep 10, 2026
361ec6b
Register workspace container type in sample plugin
DarshitChanpura Sep 10, 2026
1577069
Expect workspace as a shareable type in sample types API
DarshitChanpura Sep 10, 2026
e82c018
Make workspace reconciliation monotonic and workspace field trusted
DarshitChanpura Sep 10, 2026
eca8bdf
Merge remote-tracking branch 'upstream/main' into workspace-aware-sha…
DarshitChanpura Sep 10, 2026
7c5cf51
Make workspaces guard durable across record rewrites
DarshitChanpura Sep 10, 2026
81f704c
Guard concurrent share/patch against reconcile clobber
DarshitChanpura Sep 10, 2026
8f62005
Restore context on fetch failure; preserve parse cause; strengthen CA…
DarshitChanpura Sep 10, 2026
649432f
Tighten workspace javadoc and inline comments
DarshitChanpura Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ public void testBehaviorAfterEnabling() throws Exception {
migrateResponse.assertStatusCode(HttpStatus.SC_OK);
assertThat(
migrateResponse.bodyAsMap().get("summary"),
equalTo("Migration complete. migrated 1; skippedNoType 0; skippedExisting 0; failed 0")
equalTo("Migration complete. migrated 1; backfilledExisting 0; skippedNoType 0; skippedExisting 0; failed 0")
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ public void testResourceProtected() throws Exception {
migrateResponse.assertStatusCode(HttpStatus.SC_OK);
assertThat(
migrateResponse.bodyAsMap().get("summary"),
equalTo("Migration complete. migrated 1; skippedNoType 0; skippedExisting 0; failed 0")
equalTo("Migration complete. migrated 1; backfilledExisting 0; skippedNoType 0; skippedExisting 0; failed 0")
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@

package org.opensearch.sample.resource.feature.enabled;

import java.time.Duration;
import java.util.Map;
import java.util.Set;

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;
Expand All @@ -28,8 +30,11 @@
import org.opensearch.test.framework.cluster.TestRestClient;
import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse;

import tools.jackson.databind.JsonNode;

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.assertSearchResponse;
import static org.opensearch.sample.resource.TestUtils.ApiHelper.searchAllPayload;
import static org.opensearch.sample.resource.TestUtils.ApiHelper.searchByNamePayload;
Expand All @@ -47,6 +52,7 @@
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.putSharingInfoPayload;
import static org.opensearch.sample.utils.Constants.RESOURCE_INDEX_NAME;
import static org.opensearch.sample.utils.Constants.RESOURCE_TYPE;
import static org.opensearch.security.api.AbstractApiIntegrationTest.forbidden;
import static org.opensearch.security.api.AbstractApiIntegrationTest.ok;
Expand Down Expand Up @@ -256,6 +262,76 @@ public void testApiAccess_allAccessUser() throws Exception {
forbidden(() -> api.deleteResource(adminResId, FULL_ACCESS_USER));
}

@Test
public void testWorkspaceMembership_denyAllowDeny() throws Exception {
// A non-owner, non-shared user sees a resource ONLY while it belongs to a workspace they are a member of.
// FULL_ACCESS_USER's single security role is scoped as user_<username>__<role>; SampleResourceExtension
// resolves each security role R to workspace "ws-R", so this is the user's one accessible workspace.
final String userWorkspace = "ws-user_" + FULL_ACCESS_USER.getName() + "__shared_role";

// Resource owned by admin, initially in no workspace. FULL_ACCESS_USER is neither owner nor shared-with.
String resId = api.createSampleResourceAs(USER_ADMIN);
api.awaitSharingEntry(resId);

// DENY: not shared and not in the user's workspace -> the user's search returns no hits.
assertSearchResponse(ok(() -> api.searchResources(FULL_ACCESS_USER)), 0, null);

// ALLOW: associate the resource with the user's workspace (write the resource doc's workspaces field). The
// index listener reconciles the sharing record to the same set; DLS filters the doc's live field for reads.
setResourceWorkspaces(resId, userWorkspace);
awaitSharingRecordWorkspace(resId, true, userWorkspace);
assertSearchResponse(ok(() -> api.searchResources(FULL_ACCESS_USER)), 1, "sample");

// DENY (dissociate): clear the workspace. The listener reconciles the record to empty (removal), so neither
// the read path (DLS) nor the write path (the sharing record's workspace set) retains a stale grant.
setResourceWorkspaces(resId);
awaitSharingRecordWorkspace(resId, false, userWorkspace);
assertSearchResponse(ok(() -> api.searchResources(FULL_ACCESS_USER)), 0, null);
}

// Sets the resource doc's `workspaces` field to exactly the given ids (empty clears it), as the super admin.
private void setResourceWorkspaces(String resourceId, String... workspaceIds) {
StringBuilder arr = new StringBuilder("[");
for (int i = 0; i < workspaceIds.length; i++) {
if (i > 0) {
arr.append(",");
}
arr.append("\"").append(workspaceIds[i]).append("\"");
}
arr.append("]");
try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) {
HttpResponse resp = client.postJson(
RESOURCE_INDEX_NAME + "/_update/" + resourceId + "?refresh=true",
"{\"doc\":{\"workspaces\":" + arr + "}}"
);
resp.assertStatusCode(HttpStatus.SC_OK);
}
}

// Waits until the sharing record's `workspaces` set does (or does not) contain the given id, confirming the
// listener reconciled the record to match the resource doc.
private void awaitSharingRecordWorkspace(String resourceId, boolean shouldContain, String workspaceId) {
try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) {
Awaitility.await("sharing record for " + resourceId + (shouldContain ? " contains " : " excludes ") + workspaceId)
.pollInterval(Duration.ofMillis(500))
.atMost(Duration.ofSeconds(10))
.untilAsserted(() -> {
HttpResponse resp = client.get(RESOURCE_SHARING_INDEX + "/_doc/" + resourceId);
resp.assertStatusCode(HttpStatus.SC_OK);
JsonNode ws = resp.bodyAsJsonNode().get("_source").get("workspaces");
boolean found = false;
if (ws != null && ws.isArray()) {
for (JsonNode n : ws) {
if (workspaceId.equals(n.asString())) {
found = true;
}
}
}
assertThat(found, equalTo(shouldContain));
});
}
}

@Test
public void testApiAccess_superAdmin() {
// can see admin's resource
Expand Down
Loading
Loading