Skip to content

Workspace aware sharing records - #6374

Draft
DarshitChanpura wants to merge 8 commits into
opensearch-project:mainfrom
DarshitChanpura:workspace-aware-sharing-records
Draft

Workspace aware sharing records#6374
DarshitChanpura wants to merge 8 commits into
opensearch-project:mainfrom
DarshitChanpura:workspace-aware-sharing-records

Conversation

@DarshitChanpura

Copy link
Copy Markdown
Member

Description

[Describe what this change achieves]

  • Category (Enhancement, New feature, Bug fix, Test fix, Refactoring, Maintenance, Documentation)
  • Why these changes are required?
  • What is the old behavior before changes and new behavior after changes?

Issues Resolved

[List any issues this PR will resolve]

Is this a backport? If so, please add backport PR # and/or commits #, and remove backport-failed label from the original PR.

Do these changes introduce new permission(s) to be displayed in the static dropdown on the front-end? If so, please open a draft PR in the security dashboards plugin and link the draft PR here

Testing

[Please provide details of testing done: unit testing, integration testing and manual testing]

Check List

  • New functionality includes testing
  • New functionality has been documented
  • New Roles/Permissions have a corresponding security dashboards plugin PR
  • API changes companion pull request created
  • Commits are signed per the DCO using --signoff

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.

Let a resource belong to multiple workspaces and be discoverable by
workspace members through the existing DLS sharing mechanism.

Adds a workspace: principal namespace: workspace IDs on a resource are
projected into all_shared_principals, and a user's accessible workspaces
are added to the DLS filter, so the existing terms intersection grants
visibility via workspace membership with no new query shape.

Spike scope (read/discovery path only):
- SPI: ResourceProvider.workspacesField() (default null; additive)
- Multi-value field extraction from the index op at index time
- ResourceSharing.workspaces set: builder, XContent (omitted when empty),
  fromXContent, equals/hashCode/toString, version-guarded writeTo
- getAllPrincipals() emits workspace:<id>; DLS adds the user's workspaces
  I/O-free from an in-memory User attribute (honors hot-path no-I/O rule)
- Seed visibility from getAllPrincipals() (creator + workspaces)

Not yet addressed (follow-ups, intentionally not stubbed):
- Write path (hasPermission) cross-record resolution of workspace access
  levels from the workspace's own sharing record
- WORKSPACES_INTRODUCED_VERSION is a compile-only placeholder
- No registered NamedWriteable reader for ResourceSharing (pre-existing)
- Lucene doc-values materialization needs an integration-test spike
- User->workspaces attribute key and authc-time population are placeholders

ResourceSharingTests: 21 tests, 0 failures.
Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
Grant a user access to a resource when they have the required access
level on any workspace the resource belongs to, not just when the
resource is shared with them directly.

Generalizes the existing single-parent access recursion in
hasPermission into a fan-out over the resource's containers: its
hierarchical parent (if any) plus each of its workspaces. Each
workspace is resolved through hasPermission against the workspace's own
sharing record, so workspace collaborators and their access levels map
through the workspace type's action groups (per issue opensearch-project#6119). Access is
granted if any container grants it; evaluation short-circuits on the
first grant.

Spike notes / follow-ups:
- Workspace resource type name is a placeholder ("workspace"); the real
  type comes from the workspace provider registered via the SPI. If no
  provider is registered, the workspace branch denies cleanly.
- No cycle/depth guard yet; safe for the intended model (workspace
  records do not themselves carry workspaces) but should be added.

ResourceAccessHandlerTests: 15 tests, 0 failures.
Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
Prevent unbounded recursion when a resource inherits access from its
containers (parent and/or workspaces) and the container graph is
malformed (e.g. a workspace that transitively contains itself).

Threads a visited set of type:id keys through the permission walk;
re-encountering an already-visited resource short-circuits to false,
which is safe under the fan-out's OR semantics. The public hasPermission
signature is unchanged; a private overload carries the set.

ResourceAccessHandlerTests: 16 tests, 0 failures (adds a self-
referential-workspace cycle case).

Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
Resource sharing was introduced in 3.3 and is not yet GA, and the
workspaces field ships within that same not-yet-released feature, so no
older node speaks a wire format that omits it. The version gate (and its
placeholder constant) added nothing but a misleading TODO; serialize the
field unconditionally.

The pre-existing NamedWriteable reader gap for ResourceSharing is
unchanged and still noted as a follow-up.

Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
Avoid an N+1 sequential-GET pattern when a resource inherits access from
the workspaces it belongs to. Previously each container was resolved by
a separate recursive hasPermission call, i.e. one GET per workspace,
serially, on the privilege hot path.

Fetch all of a resource's workspace sharing records in a single mget
(they live in one index with known ids) and evaluate them in memory via
a new pure recordGrantsAction helper. The single hierarchical parent is
still resolved recursively so grandparent chains keep working, and the
visited-set cycle guard now also pre-filters workspace ids before the
batch. Workspace records are evaluated as leaves (their own share_with),
matching the flat workspace model.

ResourceAccessHandlerTests: 16 tests, 0 failures.
Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

PR Code Analyzer ❗

AI-powered 'Code-Diff-Analyzer' found issues on commit 7412d76.

Hard block: Issues at High severity or above will block this PR from merging.

PathLineSeverityDescription
src/main/java/org/opensearch/security/resources/ResourceSharingDlsUtils.java99mediumWorkspace membership is resolved from user custom attribute 'attr.internal.workspaces', which is read directly from the in-memory User object. In OpenSearch Security, custom attributes can be populated from user-controlled sources such as JWT claims. If an attacker can inject or influence the value of this attribute at authentication time, they could claim membership in arbitrary workspaces and gain read access to resources those workspaces contain, bypassing the intended access control model. The code itself acknowledges this as a 'SPIKE placeholder' with 'production wiring as an open question', meaning the trust boundary for this attribute is explicitly undefined. If this code reaches production before the attribute population mechanism is secured (e.g., enforced as server-set only), it constitutes a privilege escalation vector.

The table above displays the top 10 most important findings.

Total: 1 | Critical: 0 | High: 0 | Medium: 1 | Low: 0


Pull Requests Author(s): Please update your Pull Request according to the report above.

Repository Maintainer(s): You can bypass diff analyzer by adding label skip-diff-analyzer after reviewing the changes carefully, then re-run failed actions. To re-enable the analyzer, remove the label, then re-run all actions.


⚠️ Note: The Code-Diff-Analyzer helps protect against potentially harmful code patterns. Please ensure you have thoroughly reviewed the changes beforehand.

Thanks.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 7412d76)

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 Security concerns

Workspace-based access bypass risk:
The DLS intersection uses a workspace:<id> principal derived from a placeholder user custom attribute (attr.internal.workspaces) that is parsed from a comma-separated string with no defined authenticator to populate it. If an authenticator or admin path allows users to influence this attribute (directly or via header/claim mapping), a user could self-assert workspace membership and gain read access to any resource stamped with that workspace ID. The code itself notes production wiring is undefined; landing this without a trusted population path could enable privilege escalation.

✅ No TODO sections
🔀 No multiple PR themes
⚡ Recommended focus areas for review

Cycle Guard False Negative

The visited-set guard adds the current resource key before evaluating access. If the same resource legitimately appears on multiple branches of the container graph (e.g. two workspaces both containing the same parent workspace, a common DAG rather than a true cycle), the second visit short-circuits to false even though the resource would have granted access. The comment claims OR-semantics make this safe, but because the "first visit" recursion runs asynchronously and may itself return false while another branch could grant, the ordering-dependent short-circuit can produce a false denial for valid non-cyclic diamond-shaped inheritance graphs.

// Cycle/duplicate guard: if we've already evaluated this exact resource on this authorization walk, do not
// re-evaluate it. Returning false is safe under the fan-out's OR semantics (the first visit's result stands).
final String visitKey = resourceType + ":" + resourceId;
if (!visited.add(visitKey)) {
    LOGGER.debug("Skipping already-visited resource '{}' of type '{}' to avoid a container cycle", resourceId, resourceType);
    listener.onResponse(false);
    return;
}
Wire Format Asymmetry

writeTo now writes an additional optional string collection for workspaces, but there is no corresponding StreamInput constructor or reader that consumes this field. Any transport consumer using readNamedWriteable(ResourceSharing.class) will either fail to deserialize or read misaligned bytes for subsequent fields. The in-code comment acknowledges this as a pre-existing gap, but this change actively widens the serialized payload, making the mismatch worse. Consider gating the write behind a symmetric reader or reverting the writeTo change until the reader is wired.

// No version guard needed: workspaces ships within the resource-sharing feature (introduced in 3.3),
// which is not yet GA, so there is no older node that speaks the old wire format without this field.
// PRE-EXISTING GAP (not introduced here): ResourceSharing has no StreamInput constructor and is not
// registered in OpenSearchSecurityPlugin#getNamedWriteables, yet ShareResponse reads it via
// readNamedWriteable(ResourceSharing.class). Wiring a symmetric reader (that also reads this field) is a
// required follow-up before relying on transport round-trips.
out.writeOptionalStringCollection(workspaces == null ? null : new ArrayList<>(workspaces));
Attribute Parsing Fragility

Workspace IDs are parsed from a comma-separated custom attribute string. Any workspace ID containing a comma would be split into invalid IDs, silently granting or denying access incorrectly. Since this is on the privilege hot path and workspace IDs typically originate from user-authored saved-object metadata, either the attribute format needs to guarantee no commas, or a safer separator/encoding should be used. Also flagged as a spike placeholder — should be resolved before production use.

private static Set<String> resolveUserWorkspaces(User user) {
    String raw = user.getCustomAttributesMap() == null ? null : user.getCustomAttributesMap().get(WORKSPACES_ATTRIBUTE);
    if (raw == null || raw.isBlank()) {
        return Collections.emptySet();
    }
    Set<String> workspaces = new HashSet<>();
    for (String id : raw.split(",")) {
        String trimmed = id.trim();
        if (!trimmed.isEmpty()) {
            workspaces.add(trimmed);
        }
    }
    return workspaces;
}

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to bcc3024

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Add symmetric stream reader for new field

The writeTo method serializes the new workspaces field but there is no corresponding
reader that consumes it. Any transport code invoking
readNamedWriteable(ResourceSharing.class) (e.g. ShareResponse) will fail to parse
the stream once this field is written. Add a symmetric StreamInput constructor /
reader that reads readOptionalStringCollection in the same order, or the wire format
will be broken.

src/main/java/org/opensearch/security/resources/sharing/ResourceSharing.java [271]

 out.writeOptionalStringCollection(workspaces == null ? null : new ArrayList<>(workspaces));
+// NOTE: a symmetric StreamInput reader must be added (readOptionalStringCollection → Set) and registered
+// via getNamedWriteables before this can round-trip over the transport.
Suggestion importance[1-10]: 7

__

Why: The PR author already acknowledges in a comment that a symmetric StreamInput reader is a required follow-up. The suggestion reinforces this pre-existing gap but doesn't propose a concrete fix in improved_code (only adds a comment), limiting its impact.

Medium
General
Workspace inheritance ignores nested containers

The workspace evaluation only considers the workspace's own share_with via
recordGrantsAction and never recurses into that workspace's own containers (its
parent or its workspaces). If a workspace inherits access from a parent workspace,
users legitimately entitled via that chain will be denied. Consider recursing into
hasPermission for each workspace id (leaf-only recursion is not sufficient if
workspaces themselves are hierarchical).

src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java [294-302]

+resourceSharingIndexHandler.fetchSharingInfoForIds(workspaceIndex, workspaceIds, ActionListener.wrap(records -> {
+    for (ResourceSharing wsRecord : records.values()) {
+        if (recordGrantsAction(wsRecord, WORKSPACE_RESOURCE_TYPE, user, action)) {
+            listener.onResponse(true);
+            return;
+        }
+    }
+    checkParent(sharingInfo, action, visitedAncestors, listener);
+}, listener::onFailure));
 
-
Suggestion importance[1-10]: 5

__

Why: Valid design concern: workspaces are evaluated as leaves and won't inherit from their own containers. However, the code comments explicitly state workspaces are leaf-evaluated by design in this spike, so this may be intentional scope.

Low
Cycle-deny may mask legitimate grants

Under OR semantics, denying on a cycle-detected repeat is only safe if the "first
visit" of the same node is guaranteed to still contribute its grant. However, since
checkContainers invokes the workspace branch first and only calls checkParent (which
is what re-enters hasPermission) after workspaces fail, a cycle detected via the
parent chain returns false here without ever unwinding to a positive branch.
Consider whether cycle-detection should propagate through onFailure or a sentinel
rather than a plain false, so a genuine grant elsewhere is not masked by a cycle in
one branch.

src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java [178-183]

+final String visitKey = resourceType + ":" + resourceId;
+if (!visitedAncestors.add(visitKey)) {
+    LOGGER.debug("Skipping resource '{}' of type '{}' already on the parent chain to avoid a cycle", resourceId, resourceType);
+    outerListener.onResponse(false);
+    return;
+}
 
-
Suggestion importance[1-10]: 4

__

Why: Raises a valid theoretical concern about cycle detection masking grants, but the improved_code is identical to existing_code and offers no concrete fix. Limited actionable impact.

Low
Fix thread-context restoration in async callback

The ctx.restore() is only called on the success path. On the failure branch,
ctx.restore() is invoked but the try-with-resources block will also invoke
ctx.close() (which restores again). More importantly, if client.multiGet throws
synchronously before the listener is invoked, the try-with-resources will restore
correctly, but the explicit ctx.restore() on both branches inside the listener runs
after the try-with-resources has already restored the context on the caller thread,
potentially restoring the wrong context on the executor thread. Consider removing
the explicit ctx.restore() calls and relying solely on try-with-resources, or
wrapping the listener with ContextPreservingActionListener.

src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java [586-587]

 client.multiGet(mget, ActionListener.wrap(mres -> {
-    ctx.restore();
Suggestion importance[1-10]: 3

__

Why: The suggestion's reasoning is partially incorrect—ctx.restore() inside the listener runs on the executor thread and is the standard pattern used elsewhere in the file. The improved_code also just removes one line without proposing a proper alternative.

Low

Previous suggestions

Suggestions up to commit 7412d76
CategorySuggestion                                                                                                                                    Impact
Possible issue
Add symmetric wire reader for new field

writeTo was updated to serialize workspaces, but there is no symmetric StreamInput
constructor/reader that reads this field. Any receiver deserializing via
readNamedWriteable(ResourceSharing.class) will either fail or drop the field,
causing wire-level incompatibility and lost workspace membership across nodes. Add a
matching StreamInput reader that consumes readOptionalStringCollection in the same
order, or gate the write behind a feature flag until the reader lands.

src/main/java/org/opensearch/security/resources/sharing/ResourceSharing.java [271]

+// TODO: symmetric reader must consume readOptionalStringCollection() in the same order.
 out.writeOptionalStringCollection(workspaces == null ? null : new ArrayList<>(workspaces));
Suggestion importance[1-10]: 7

__

Why: Correctly identifies that adding writeOptionalStringCollection without a matching reader creates wire-format asymmetry, though the PR comments note the pre-existing gap in reader wiring. Still an important correctness concern for transport round-trips.

Medium
Security
Prevent workspace-attribute spoofing escalation

Reading workspace membership from a user-controlled comma-separated attribute is a
privilege-escalation risk if that attribute path can be influenced by external
identity providers or user-defined role mappings. Anyone able to set
attr.internal.workspaces on their user would gain read access to all resources in
those workspaces via DLS. Restrict the source to a trusted internal attribute set
only by the server-side workspace resolution, and reject/ignore any
externally-supplied value.

src/main/java/org/opensearch/security/resources/ResourceSharingDlsUtils.java [98-111]

 private static Set<String> resolveUserWorkspaces(User user) {
+    // MUST read only from a server-populated, non-user-settable attribute to prevent privilege escalation.
     String raw = user.getCustomAttributesMap() == null ? null : user.getCustomAttributesMap().get(WORKSPACES_ATTRIBUTE);
     if (raw == null || raw.isBlank()) {
         return Collections.emptySet();
     }
     Set<String> workspaces = new HashSet<>();
     for (String id : raw.split(",")) {
         String trimmed = id.trim();
         if (!trimmed.isEmpty()) {
             workspaces.add(trimmed);
         }
     }
     return workspaces;
 }
Suggestion importance[1-10]: 7

__

Why: Valid security concern about a user-settable attribute being used to grant workspace access. However, the code is explicitly flagged as a spike placeholder, and the improved_code is essentially identical to existing_code (only a comment added), reducing its actionable impact.

Medium
General
Ensure admin bypass precedes cycle guard

The cycle guard is applied after the admin check but before evaluating the resource.
However, since checkContainers already pre-adds workspace keys to visited before
calling into hasPermission for the parent, and this method also adds the same key on
entry, there is redundant tracking. More critically, adding to visited before the
admin bypass is fine, but admins should short-circuit before the visited-check to
guarantee admins can never be denied by a cycle bug. Move the admin check above the
visited guard.

src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java [172-179]

-// Cycle/duplicate guard: if we've already evaluated this exact resource on this authorization walk, do not
-// re-evaluate it. Returning false is safe under the fan-out's OR semantics (the first visit's result stands).
+if (adminDNs.isAdmin(user)) {
+    LOGGER.debug("User '{}' is admin, automatically granted permission on '{}'", user.getName(), resourceId);
+    listener.onResponse(true);
+    return;
+}
 final String visitKey = resourceType + ":" + resourceId;
 if (!visited.add(visitKey)) {
     LOGGER.debug("Skipping already-visited resource '{}' of type '{}' to avoid a container cycle", resourceId, resourceType);
     listener.onResponse(false);
     return;
 }
Suggestion importance[1-10]: 6

__

Why: Valid observation: placing the admin check before the visited guard ensures admins are never denied due to cycle detection. Minor but sensible correctness/robustness improvement on the auth hot path.

Low
Clarify or fix workspace container inheritance

Workspace records themselves may have parents or nested workspaces, but this
evaluation only calls recordGrantsAction (a direct-only check) on each fetched
workspace record and never recurses into their containers. If a workspace inherits
access from its own parent/workspaces, that inheritance is silently ignored,
producing incorrect denials. Consider recursing via hasPermission for each workspace
id (still batched by first prefetching), or document explicitly that workspaces
cannot inherit.

src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java [285-293]

 resourceSharingIndexHandler.fetchSharingInfoForIds(workspaceIndex, workspaceIds, ActionListener.wrap(records -> {
     for (ResourceSharing wsRecord : records.values()) {
         if (recordGrantsAction(wsRecord, WORKSPACE_RESOURCE_TYPE, user, action)) {
             listener.onResponse(true);
             return;
         }
     }
+    // NOTE: workspace records' own containers are intentionally not traversed here; document this contract.
     checkParent(sharingInfo, action, visited, listener);
 }, listener::onFailure));
Suggestion importance[1-10]: 6

__

Why: Legitimate design concern: workspace records are evaluated only directly, not recursively via their own containers, which may cause missed inheritance. Worth documenting or fixing depending on intended semantics.

Low
Suggestions up to commit ac9b426
CategorySuggestion                                                                                                                                    Impact
Possible issue
Diamond inheritance may be incorrectly denied

Returning false when a resource is already visited is unsafe: if the same resource
is reachable via two container branches (e.g. shared parent/workspace ancestor — a
diamond, not a cycle), the second branch will short-circuit to deny before the first
branch's async fetch completes, potentially causing an incorrect denial depending on
evaluation ordering. Consider tracking only in-flight ids for true cycle detection,
or fanning out uniquely before recursion so duplicates cannot occur.

src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java [174-179]

 final String visitKey = resourceType + ":" + resourceId;
 if (!visited.add(visitKey)) {
-    LOGGER.debug("Skipping already-visited resource '{}' of type '{}' to avoid a container cycle", resourceId, resourceType);
+    LOGGER.debug("Skipping already-visited resource '{}' of type '{}' (already evaluated on another branch)", resourceId, resourceType);
     listener.onResponse(false);
     return;
 }
Suggestion importance[1-10]: 6

__

Why: Valid concern about the visited-set semantics: returning false on re-encounter can incorrectly deny in diamond inheritance scenarios where OR-semantics should apply. However, the improved_code is essentially identical to existing_code (only log message differs), so the impact is limited to raising awareness.

Low
Premature visited-marking blocks valid evaluation

Workspace ids are marked as visited before their sharing records are actually
fetched/evaluated. If the resource itself belongs to workspace W and W is later
reached again via a parent chain, the second visit will short-circuit to deny even
though W was never evaluated on this branch (the mget could still be in-flight).
Mark ids as visited only when actually evaluated, or use a separate "queued" set
distinct from "evaluated".

src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java [274-279]

 final List<String> workspaceIds = new ArrayList<>();
+final Set<String> seenLocal = new HashSet<>();
 for (String workspaceId : sharingInfo.getWorkspaces()) {
-    if (visited.add(WORKSPACE_RESOURCE_TYPE + ":" + workspaceId)) {
+    if (seenLocal.add(workspaceId)) {
         workspaceIds.add(workspaceId);
     }
 }
+// Mark visited only when a workspace record is actually evaluated (inside the mget callback), not up front.
Suggestion importance[1-10]: 6

__

Why: Valid observation about the interaction between eagerly marking workspaces as visited and later parent recursion potentially skipping legitimate evaluation. The concern is reasonable but the improved_code is incomplete (comment placeholder).

Low
Serialization lacks symmetric deserializer

The corresponding StreamInput reader for ResourceSharing does not exist (as
acknowledged in the comment), so writing this new field will break wire
compatibility with any receiver that attempts to deserialize the object — including
ShareResponse which already reads it via readNamedWriteable. This should be resolved
before merging rather than left as a follow-up, otherwise transport calls will fail
at runtime.

src/main/java/org/opensearch/security/resources/sharing/ResourceSharing.java [271]

+// TODO: add symmetric StreamInput constructor + register in NamedWriteables before enabling transport round-trips.
 out.writeOptionalStringCollection(workspaces == null ? null : new ArrayList<>(workspaces));
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies a pre-existing gap that the PR itself acknowledges in the comment, but the improved_code merely adds a TODO comment. The concern is valid but the PR already documents this as a known follow-up.

Low
General
Delimiter-based parsing corrupts ids containing commas

Using a comma-separated custom attribute for workspace ids is fragile: workspace ids
can legitimately contain commas or whitespace (they are frequently user-generated or
UUID-like but not guaranteed), and no escaping is applied when the attribute is
populated. Consider reading a structured attribute (e.g. a List custom attribute or
a JSON-encoded value) to avoid silent data corruption when ids contain the
delimiter.

src/main/java/org/opensearch/security/resources/ResourceSharingDlsUtils.java [99-110]

 String raw = user.getCustomAttributesMap() == null ? null : user.getCustomAttributesMap().get(WORKSPACES_ATTRIBUTE);
 if (raw == null || raw.isBlank()) {
     return Collections.emptySet();
 }
+// TODO: replace comma-splitting with a structured attribute; ids may contain commas.
 Set<String> workspaces = new HashSet<>();
 for (String id : raw.split(",")) {
     String trimmed = id.trim();
     if (!trimmed.isEmpty()) {
         workspaces.add(trimmed);
     }
 }
 return workspaces;
Suggestion importance[1-10]: 4

__

Why: Valid point about the fragility of comma-separated parsing, but the PR explicitly labels this as a spike placeholder with production wiring as an open item. The improved_code only adds a TODO comment without fixing the issue.

Low
Suggestions up to commit 76670dd
CategorySuggestion                                                                                                                                    Impact
General
Surface per-item mget failures explicitly

When an individual mget item fails (e.g. shard-level failure), it is silently
skipped, which can lead to false denials on the authorization path because a
workspace that actually grants access may be treated as non-existent. Log the
failure and consider propagating it to the listener so callers can distinguish "not
found" from "fetch failed".

src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java [589-590]

 client.multiGet(mget, ActionListener.wrap(mres -> {
     ctx.restore();
     Map<String, ResourceSharing> records = new HashMap<>();
     for (MultiGetItemResponse item : mres.getResponses()) {
-        if (item == null || item.isFailed()) continue;
+        if (item == null) continue;
+        if (item.isFailed()) {
+            LOGGER.warn("mget item failed for id={} in {}: {}", item.getId(), resourceSharingIndex, item.getFailure().getFailure());
+            continue;
+        }
Suggestion importance[1-10]: 6

__

Why: Valid observation: silently skipping failed mget items can cause false denials on the authorization path. Logging the failure improves diagnosability, though propagating errors would be a semantic choice.

Low
Possible issue
Ensure visited-key prefix consistency

The visited set is added to twice for workspaces: once in checkContainers (to filter
the mget input) and again here when hasPermission is entered for a workspace record.
However, hasPermission is not invoked on workspace records (they are evaluated
in-memory via recordGrantsAction), so the pre-filter in checkContainers is the only
guard, and any parent-chain recursion that leads back to a workspace id won't be
caught because workspace ids were added under the workspace: prefix while
hasPermission uses resourceType + ":" + resourceId. Ensure the prefix used for the
visited key is consistent with the actual resourceType returned by the workspace
provider (not the placeholder "workspace") to avoid cycle-guard bypass.

src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java [175-179]

 if (!visited.add(visitKey)) {
     LOGGER.debug("Skipping already-visited resource '{}' of type '{}' to avoid a container cycle", resourceId, resourceType);
     listener.onResponse(false);
     return;
 }
+// NOTE: keys added in checkContainers must use the same resourceType string used here.
Suggestion importance[1-10]: 6

__

Why: Legitimate concern: checkContainers uses the WORKSPACE_RESOURCE_TYPE placeholder for visited keys, which may not match the actual resource type of the workspace, potentially bypassing the cycle guard. The improved_code is essentially the same as existing, only adds a comment.

Low
Wire-format write lacks symmetric reader

writeTo now writes an additional trailing field, but there is no symmetric
StreamInput reader that consumes it. As noted in the comment, ShareResponse uses
readNamedWriteable(ResourceSharing.class); without a matching reader, any transport
round-trip will either fail or leave the stream misaligned and corrupt subsequent
reads. Add the reader before merging, or gate the write behind the reader being in
place, otherwise this is a breaking wire-format change.

src/main/java/org/opensearch/security/resources/sharing/ResourceSharing.java [271]

+// TODO: must add symmetric StreamInput constructor / reader before this write is safe on the wire.
 out.writeOptionalStringCollection(workspaces == null ? null : new ArrayList<>(workspaces));
Suggestion importance[1-10]: 5

__

Why: The suggestion reiterates a gap already explicitly acknowledged in the PR comment (pre-existing lack of symmetric reader). The improved_code merely adds a TODO comment without fixing the underlying issue.

Low
Security
Avoid comma-split parsing for IDs

Parsing workspace IDs from a comma-separated attribute is fragile: workspace IDs
that legitimately contain a comma will be split incorrectly, silently over-granting
or under-granting access via DLS. Even as a spike, use a structured attribute (list)
or escape/validate IDs to avoid ambiguity on the security-critical read path.

src/main/java/org/opensearch/security/resources/ResourceSharingDlsUtils.java [99-110]

-String raw = user.getCustomAttributesMap() == null ? null : user.getCustomAttributesMap().get(WORKSPACES_ATTRIBUTE);
-if (raw == null || raw.isBlank()) {
+Object rawObj = user.getCustomAttributesMap() == null ? null : user.getCustomAttributesMap().get(WORKSPACES_ATTRIBUTE);
+if (rawObj == null) {
     return Collections.emptySet();
 }
 Set<String> workspaces = new HashSet<>();
-for (String id : raw.split(",")) {
-    String trimmed = id.trim();
-    if (!trimmed.isEmpty()) {
-        workspaces.add(trimmed);
+if (rawObj instanceof Collection<?>) {
+    for (Object id : (Collection<?>) rawObj) {
+        if (id != null && !id.toString().isBlank()) workspaces.add(id.toString().trim());
+    }
+} else {
+    String raw = rawObj.toString();
+    if (!raw.isBlank()) {
+        for (String id : raw.split(",")) {
+            String trimmed = id.trim();
+            if (!trimmed.isEmpty()) workspaces.add(trimmed);
+        }
     }
 }
 return workspaces;
Suggestion importance[1-10]: 5

__

Why: Reasonable concern for a security-relevant path, though the code is explicitly marked as a spike placeholder and the attribute mechanism is expected to change before production.

Low

Make the resource-sharing migrate endpoint workspace-aware so workspaces
that predate resource sharing carry their membership into the sharing
records created during migration.

When a provider declares workspacesField(), read the (multi-valued) set
of workspace IDs off each source-doc search hit and set it on the built
ResourceSharing record, so getAllPrincipals() emits workspace:<id> and
DLS/write-path inheritance work for backfilled records exactly as for
records indexed while the feature is on. Providers that do not declare
the field are unaffected.

Extraction is factored into a package-private static extractWorkspaces
helper (array or scalar, blank ids ignored, dot-notation paths), the
migrate-path counterpart of ResourcePluginInfo.extractMultiValuedField-
FromIndexOp.

Does not address updating already-migrated (skippedExisting) records or
materializing workspace collaborator records from frontend ACLs; both
are tracked as follow-ups.

MigrateResourceSharingInfoApiActionTests: 13 tests, 0 failures.
Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
@DarshitChanpura
DarshitChanpura force-pushed the workspace-aware-sharing-records branch from ac9b426 to 7412d76 Compare August 8, 2026 00:30
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 7412d76

Two findings from the PR code analyzer:

1. (Medium, security) DLS resolved workspace membership from a
   user-influenceable custom attribute, which feeds authorization and
   could let a user claim arbitrary workspace membership and read those
   workspaces' resources. Since no trusted server-set source of
   membership is wired yet, disable the resolver (returns empty) with an
   explicit server-set-only contract, removing the escalation vector
   until the trusted mechanism exists.

2. (Robustness) The container cycle guard used a global visited set and
   denied re-entry, which could falsely deny a node reachable from more
   than one branch in a DAG. Scope the guard to the current ancestor
   (parent) chain and remove each key when its node resolves; workspaces
   are leaf-evaluated and no longer touch the set at all, so sibling
   branches can never falsely deny each other.

Resources test package: 95 tests, 0 failures.

Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
@codecov

codecov Bot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 52.14286% with 67 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.30%. Comparing base (5e8e5f1) to head (bcc3024).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...ecurity/resources/ResourceSharingIndexHandler.java 18.60% 34 Missing and 1 partial ⚠️
...arch/security/resources/ResourceAccessHandler.java 78.57% 6 Missing and 3 partials ⚠️
...nsearch/security/resources/ResourcePluginInfo.java 0.00% 7 Missing ⚠️
...ch/security/resources/sharing/ResourceSharing.java 70.00% 4 Missing and 2 partials ⚠️
...i/migrate/MigrateResourceSharingInfoApiAction.java 76.19% 2 Missing and 3 partials ⚠️
...ch/security/resources/ResourceSharingDlsUtils.java 25.00% 2 Missing and 1 partial ⚠️
...arch/security/resources/ResourceIndexListener.java 0.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #6374      +/-   ##
==========================================
- Coverage   75.33%   75.30%   -0.03%     
==========================================
  Files         456      456              
  Lines       30075    30251     +176     
  Branches     4564     4596      +32     
==========================================
+ Hits        22657    22781     +124     
- Misses       5297     5337      +40     
- Partials     2121     2133      +12     
Files with missing lines Coverage Δ
...earch/security/spi/resources/ResourceProvider.java 83.33% <100.00%> (+3.33%) ⬆️
...arch/security/resources/ResourceIndexListener.java 93.75% <0.00%> (-3.03%) ⬇️
...ch/security/resources/ResourceSharingDlsUtils.java 66.66% <25.00%> (-7.25%) ⬇️
...i/migrate/MigrateResourceSharingInfoApiAction.java 75.79% <76.19%> (-0.18%) ⬇️
...ch/security/resources/sharing/ResourceSharing.java 78.39% <70.00%> (-1.06%) ⬇️
...nsearch/security/resources/ResourcePluginInfo.java 79.71% <0.00%> (-4.26%) ⬇️
...arch/security/resources/ResourceAccessHandler.java 73.68% <78.57%> (+0.39%) ⬆️
...ecurity/resources/ResourceSharingIndexHandler.java 65.01% <18.60%> (-2.59%) ⬇️

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant