Skip to content

Create parent-linked sharing entries for child resources written without a user - #6373

Merged
DarshitChanpura merged 5 commits into
opensearch-project:mainfrom
DarshitChanpura:fix/child-resource-sharing-no-user
Aug 21, 2026
Merged

Create parent-linked sharing entries for child resources written without a user#6373
DarshitChanpura merged 5 commits into
opensearch-project:mainfrom
DarshitChanpura:fix/child-resource-sharing-no-user

Conversation

@DarshitChanpura

@DarshitChanpura DarshitChanpura commented Aug 6, 2026

Copy link
Copy Markdown
Member

Description

ResourceIndexListener.postIndex requires an authenticated user in the thread context to create a resource-sharing entry. When none is present, it fails with an uncaught NPE (null user subject) or skips silently at debug level. Resources written in a genuinely user-less context — scheduled jobs running under job-scheduler (e.g. scheduled report instances), provisioning steps executed under system context — therefore never receive sharing entries and stay permanently invisible to the resource-sharing APIs, including to the owner of the parent resource that triggered them.

Changes

  • Null-safe user-subject extraction
  • No user + provider declares a parent (parentType/parentIdField): the sharing entry is created by inheriting tenant/created_by from the parent's sharing record, linked via parentType/parentId so ResourceAccessHandler delegates evaluation to the parent
  • No user + no parent: skip is logged at WARN (previously silent), making the failure mode visible to operators
  • Entry-indexing failures now log at WARN instead of debug

Testing

New integration tests (SystemContextChildResourceTests, sample plugin hierarchy, all passing) index resource documents through the internal node client — no user in context, mirroring plugin/system-subject writes:

  • child resources receive a parent-linked entry inheriting the parent owner; sharing the parent grants access to the system-created child
  • parent-less resources are skipped without an entry
  • children referencing a missing parent record are skipped

Investigation note (correction)

This PR was initially motivated by on-demand report instances missing sharing records on a 3.8.0 snapshot. Deeper investigation showed that case was actually caused by listener-attachment timing on that build: the listener attaches per-index in onIndexModule filtered by protected_types, so a type added to the dynamic setting after its index was already open never got a listener until restart (current main attaches based on the unfiltered registered set, so main appears immune). On-demand instance writes do carry the authenticated user and work once the listener is attached. The user-less gap addressed by this PR remains real for scheduled/system-context writes, as covered by the new tests.

Companion PRs

Category

Bug fix

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@DarshitChanpura

Copy link
Copy Markdown
Member Author

Companion reporting PR: opensearch-project/reporting#

DarshitChanpura added a commit to DarshitChanpura/flow-framework that referenced this pull request Aug 7, 2026
Workflow state documents track the provisioning/execution state of a
workflow template and have no independent access semantics, yet they were
registered as a standalone resource type: access to them did not follow
the parent workflow's shares, and state documents written without an
authenticated user in the thread context (provisioning steps executed
under system context) receive no sharing records at all.

Declaring parentType/parentIdField on the workflow_state provider makes
state documents inherit access from their workflow via the already-mapped
workflow_id field.

Requires opensearch-project/security#6373 for state documents written
under system context to receive parent-linked sharing entries.

Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
…out a user

ResourceIndexListener.postIndex previously required an authenticated user
in the thread context to create a resource-sharing entry, and skipped
silently (debug log, or NPE on a null subject) when one was absent. Writes
performed under a plugin or system subject — e.g. reporting's on-demand
report instances indexed via PluginClient, or scheduled jobs running under
job-scheduler — therefore never received sharing entries, leaving those
resources permanently invisible to the resource-sharing APIs, including
to their creators.

With this change:
- The user subject is extracted null-safely.
- When no user is present and the resource's provider declares a parent
  (parentType/parentIdField), the sharing entry is created by inheriting
  tenant and created_by from the parent's sharing record, linked via
  parentType/parentId so access evaluation delegates to the parent.
- When no user is present and no parent is declared, the skip is now
  logged at WARN instead of silently at debug, making this failure mode
  visible to operators.
- Failures to index sharing entries are also logged at WARN instead of
  debug.

Companion change: opensearch-project/reporting declares report-instance
as a child of report-definition to use this path.

Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
Verifies via the sample plugin hierarchy that resources indexed without
an authenticated user in the thread context (internal node client,
mirroring plugin-subject writes):
- child resources receive a parent-linked sharing entry inheriting the
  parent owner, and parent-level shares grant access to them
- parent-less resources are skipped (no entry created)
- children referencing a missing parent record are skipped

Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
@DarshitChanpura
DarshitChanpura force-pushed the fix/child-resource-sharing-no-user branch from 72c9f2d to a34fb50 Compare August 7, 2026 21:13
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 9d27c4f)

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ Recommended focus areas for review

Tenant Mismatch Risk

When a child resource is created without an authenticated user, the sharing entry inherits tenant and createdBy from the parent's sharing record. If the parent was created under a specific tenant but the child write occurs in a system/plugin context that should logically belong to a different tenant (or none), the child will silently be attributed to the parent's tenant. This may be the intended design, but it should be verified — especially when multi-tenancy is enabled — as it can lead to cross-tenant visibility if a parent's tenant differs from the effective context of the child write.

ResourceSharing sharingInfo = ResourceSharing.builder()
    .resourceId(resourceId)
    .resourceType(resourceType)
    .tenant(parentSharing.getTenant())
    .createdBy(parentSharing.getCreatedBy())
    .parentType(parentType)
    .parentId(parentId)
    .build();
this.resourceSharingIndexHandler.indexResourceSharing(resourceIndex, sharingInfo, listener);

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to 9d27c4f

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Guard against null parent id when building

When user != null and parentType != null, the code sets parentId without validating
it is non-null, which can lead to an inconsistent sharing entry if the parent id
field is missing from the source. Guard the parent-attribution branch to only set
parent fields when parentId is also non-null, consistent with the user-less path.

src/main/java/org/opensearch/security/resources/ResourceIndexListener.java [124-140]

 if (user != null) {
     try {
         // User.getRequestedTenant() is null if multi-tenancy is disabled
         ResourceSharing.Builder builder = ResourceSharing.builder()
             .resourceId(resourceId)
             .resourceType(resourceType)
             .tenant(user.getRequestedTenant())
             .createdBy(new CreatedBy(user.getName()));
-        if (parentType != null) {
+        if (parentType != null && parentId != null) {
             builder.parentType(parentType).parentId(parentId);
         }
         this.resourceSharingIndexHandler.indexResourceSharing(resourceIndex, builder.build(), listener);
     } catch (IOException e) {
         log.warn("Failed to create a resource sharing entry for resource: {}", resourceId, e);
     }
     return;
 }
Suggestion importance[1-10]: 6

__

Why: Valid consistency improvement: the user-less path skips creation when parentId is null, but the user path would set parentType with a null parentId, potentially causing an inconsistent sharing entry. Moderate impact since it's an edge case.

Low

Previous suggestions

Suggestions up to commit 8dfd22f
CategorySuggestion                                                                                                                                    Impact
General
Validate missing parent id for child resource

When a user is present but parentType is declared and parentId is null (e.g., child
resource missing the parent id field), the code silently creates a sharing entry
with parentType set but parentId null. Guard against this case to avoid inconsistent
sharing records, matching the validation applied in the user-less branch.

src/main/java/org/opensearch/security/resources/ResourceIndexListener.java [124-140]

 if (user != null) {
     try {
         // User.getRequestedTenant() is null if multi-tenancy is disabled
         ResourceSharing.Builder builder = ResourceSharing.builder()
             .resourceId(resourceId)
             .resourceType(resourceType)
             .tenant(user.getRequestedTenant())
             .createdBy(new CreatedBy(user.getName()));
         if (parentType != null) {
+            if (parentId == null) {
+                log.warn("Skipping resource-sharing entry for child resource {}: parent id field {} missing.", resourceId, provider.parentIdField());
+                return;
+            }
             builder.parentType(parentType).parentId(parentId);
         }
         this.resourceSharingIndexHandler.indexResourceSharing(resourceIndex, builder.build(), listener);
     } catch (IOException e) {
         log.warn("Failed to create a resource sharing entry for resource: {}", resourceId, e);
     }
     return;
 }
Suggestion importance[1-10]: 5

__

Why: The suggestion addresses a real edge case where a child resource with an authenticated user could produce a sharing entry with parentType set but a null parentId, resulting in an inconsistent record. It's a reasonable consistency improvement, though the practical impact depends on how likely parentId is null in this branch.

Low
Suggestions up to commit a34fb50
CategorySuggestion                                                                                                                                    Impact
Possible issue
Guard against null parentId when building entry

When a user is authenticated and the resource declares a parent, parentId may still
be null if the parent field is missing from the document. Persisting a sharing entry
with parentType set but parentId null yields an inconsistent record that cannot be
resolved by parent-based access checks. Guard against null parentId similarly to the
user-less branch.

src/main/java/org/opensearch/security/resources/ResourceIndexListener.java [124-140]

 if (user != null) {
     try {
         // User.getRequestedTenant() is null if multi-tenancy is disabled
         ResourceSharing.Builder builder = ResourceSharing.builder()
             .resourceId(resourceId)
             .resourceType(resourceType)
             .tenant(user.getRequestedTenant())
             .createdBy(new CreatedBy(user.getName()));
-        if (parentType != null) {
+        if (parentType != null && parentId != null) {
             builder.parentType(parentType).parentId(parentId);
         }
         this.resourceSharingIndexHandler.indexResourceSharing(resourceIndex, builder.build(), listener);
     } catch (IOException e) {
         log.warn("Failed to create a resource sharing entry for resource: {}", resourceId, e);
     }
     return;
 }
Suggestion importance[1-10]: 6

__

Why: Valid concern: if parentType is set but parentId is null (missing parent field in the doc), the sharing entry would have inconsistent parent metadata. Adding a null check improves data consistency, though the actual runtime impact depends on whether such states are reachable in practice.

Low

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.92308% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.43%. Comparing base (f9e24a7) to head (9d27c4f).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...arch/security/resources/ResourceIndexListener.java 76.92% 7 Missing and 2 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #6373      +/-   ##
==========================================
+ Coverage   75.40%   75.43%   +0.03%     
==========================================
  Files         456      456              
  Lines       30255    30280      +25     
  Branches     4575     4580       +5     
==========================================
+ Hits        22815    22843      +28     
+ Misses       5304     5302       -2     
+ Partials     2136     2135       -1     
Files with missing lines Coverage Δ
...arch/security/resources/ResourceIndexListener.java 89.53% <76.92%> (-7.24%) ⬇️

... and 10 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.

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

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

PR Code Analyzer ❗

AI-powered 'Code-Diff-Analyzer' found issues on commit 9d27c4f.

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

PathLineSeverityDescription
src/main/java/org/opensearch/security/resources/ResourceIndexListener.java160mediumWhen no authenticated user exists in thread context, the code inherits the parent resource's sharing entry (including createdBy and tenant) for the child resource. The parentId value comes directly from the indexed document's field (provider.parentIdField()), meaning any code path able to write to the resource index without an authenticated user could craft a document pointing to a high-privilege parent and inherit that parent's ownership/sharing permissions. The design is intentional and well-documented for plugin/system writes, but the trust placed in the parentId field from document content—without additional authorization checks—creates a potential privilege-inheritance vector if write access to the monitored index is insufficiently restricted.

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

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 8dfd22f

…r-less

The SystemContextChildResourceTests class comment cited reporting's on-demand
report instances as an example of a user-less write. They actually stash-then-
restore the caller's context (PluginBaseAction), so the authenticated user is
present when the instance is indexed and postIndex attributes it normally --
matching this PR's investigation-note correction. Update the doc to cite
genuinely user-less writes (scheduled jobs under job-scheduler, system/
provisioning-context) and note the on-demand distinction.

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

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 9d27c4f

@DarshitChanpura
DarshitChanpura merged commit bb55e64 into opensearch-project:main Aug 21, 2026
113 of 114 checks passed
DarshitChanpura added a commit to DarshitChanpura/alerting that referenced this pull request Aug 24, 2026
…, comments stash

Review feedback on opensearch-project#2180 (riysaxen-amzn):

- Super-admin visibility: the RSC filter branch preceded the `user == null`
  (super-admin) branch in the alerts/workflow-alerts/comment-search/destinations
  read paths, so a super-admin got filtered to only shared resources. Check
  `user == null` first so super-admin (and the security-disabled case) sees
  everything even under resource sharing.

- Bug: getAccessibleAlertIDs (and the legacy getFilteredAlertIDs) never set a
  search size, capping alert resolution at the default 10 and silently dropping
  comments for alerts beyond the first 10. Set size to MAX_SEARCH_SIZE.

- rbac_roles hardening: validation was skipped under RSC. Since the feature flag
  is dynamic, validate caller-supplied rbac_roles regardless of RSC so a
  non-admin can't persist roles they don't hold that would gate access if RSC is
  later disabled.

- Comments-history index bootstrap now runs on the plugin subject (stashed in
  the comment index action's start()); previously a non-admin caller's
  indices().exists() threw under RSC and the request hung.

- Destinations: super-admin now runs a direct (non-DLS) search so it isn't
  filtered by the resource-sharing DLS path.

- Document the index.max_terms_count bound at the monitor/workflow-id term
  filters; derive the sharing index name from the config index constant in test
  helpers rather than hardcoding.

Subordinate-resource alert/comment access tests remain @ignore'd pending the
child-resource sharing model in opensearch-project/security#6373 (updated the
FIXMEs to reference it). Verified SecureResourceSharingMonitorRestApiIT: 31 tests,
3 skipped, 0 failures under the resource-sharing variant.

Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
riysaxen-amzn pushed a commit to opensearch-project/alerting that referenced this pull request Aug 25, 2026
* Onboards to Resource Sharing and Authorization

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

* Adds tests for resource sharing feature

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

* Fix resource types

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

* Filter alerts by accessible monitor IDs when resource sharing is enabled

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

* Adds a default access level

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

* Removes duplicate sec plugin zip loading

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

* Route access control through resource-sharing framework when enabled

Skip backend-role validation, permission checks, and filter injection
across all transport actions when the resource-sharing client is set.
For primary resources (monitors, workflows), rely on the security
plugin's DLS at the index layer. For subordinate resources (comments),
scope results by accessible monitor IDs via getAccessibleResourceIds.

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

* Avoid capturing ResourceSharingClient in lambdas to prevent NoClassDefFoundError

Storing the RSC accessor result in a local val that lambdas close over
forces the JVM to link ResourceSharingClient when the closure is created.
Without the security plugin installed at runtime that class is absent,
crashing the node with NoClassDefFoundError. Call the accessor fresh
inside the lambda instead.

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

* Route shared-resource searches through PluginClient when
  resource sharing is enabled

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

* Expand resource-sharing IT coverage over transport-level interception

Rewrites SecureResourceSharingMonitorRestApiIT to exercise the full
matrix of security plugin ActionFilter paths that DocRequest enables:
- GET / UPDATE / DELETE with insufficient and sufficient share levels
- SEARCH DLS filtering per user
- alerts subordinate to monitor share via getAccessibleResourceIds
- SHARE and REVOKE round-trips

Users no longer carry all_access so RSC is the sole authorization gate.
Also updates existing unit tests to pass the new PluginClient constructor
parameter on the affected transport actions.

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

* Decouple ResourceSharingClientAccessor from security-spi type at class-load time

Store the client as Any? and return Any? from getResourceSharingClient()
so the JVM does not resolve ResourceSharingClient when loading the
accessor class. This prevents NoClassDefFoundError in test clusters that
run without the security plugin installed. Callers cast to
ResourceSharingClient inside null-guarded blocks where the security
plugin is guaranteed to be present.

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

* Gate resource-sharing paths on isFeatureEnabledForType

- Introduce ResourceSharingUtils with MONITOR_RESOURCE_TYPE constant and
  shouldUseResourceAuthz() helper, mirroring reporting plugin's pattern.
- Replace all `rsc != null` / `rsc == null` checks in transport actions
  with shouldUseResourceAuthz() so admin flows (and non-RSC deployments)
  fall through to the existing filter-by-backend-roles path when the
  security plugin is present but the RSC feature flag is disabled.
- Move the "monitor" literal out of every call site into a single
  constant referenced by both the extension and the utility helper.

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

* Cover full access-level matrix in resource-sharing ITs

Adds tests for scenarios missing from the initial suite:
- read-write share: can delete but cannot re-share (share permission
  belongs only to full-access)
- read-write share: owner sees edits made by the shared user
- full-access share: owner sees the deletion made by the shared user

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

* Expand resource-sharing IT coverage to a full behavior matrix

Reorganizes the suite around scenarios rather than API surfaces and adds
coverage for cases the earlier version missed:

- Owner-side positive controls (owner can always get / update / delete)
- Default-deny on every mutating action (get, update, delete, re-share)
- Explicit per-access-level positive and negative assertions
  (read-only, read-write, full-access) including "read-write cannot
  re-share" — the share permission belongs only to full-access
- Third-user isolation: share to bob does not grant carol access
- Cross-resource isolation: share on monitor A does not grant access to B
- Search DLS visibility (owned, shared, other-users')
- Subordinate resources: alerts and comments inherit monitor access;
  acknowledge and comment require read-write
- Downgrade: re-sharing at a lower level narrows permissions
- Revoke: removes access; does not affect other users' shares

Also introduces carol as a third user and switches to constants
(RS_ALICE/RS_BOB/RS_CAROL, READ_ONLY/READ_WRITE/FULL_ACCESS) to keep
assertions readable.

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

* Fix resource-sharing IT setup: batch role mapping, add index perms

- Map all three users (alice, bob, carol) to alerting_full_access in a
  single PUT rolesmapping call; the previous per-user PUTs replaced each
  other, leaving only the last-created user with the role.
- Create a shared test index and grant all three users index-level read
  access to it, then point the sample monitor's SearchInput at that
  index. Without this the monitor create fails at the security plugin's
  index-permission check ("User doesn't have read permissions for one or
  more configured index []").
- Clean up the test index and its role/rolesmapping in @after.

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

* Require explicit resourceType arg to shouldUseResourceAuthz

Removes the default parameter and updates every call site to pass
ResourceSharingUtils.MONITOR_RESOURCE_TYPE explicitly. Forces callers
to state which resource type they are gating on and future-proofs
against alerting registering additional resource types.

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

* Configure protected_types for resource sharing in integTest cluster

The security plugin's share API rejects unknown resource types with
"No allowed values configured for resource_type" when the experimental
resource_sharing feature is on but protected_types is not set. Add the
setting so the plugin recognizes "monitor" as a protected type.

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

* Bump common-utils dependency to track opensearch_build (3.8.0.0)

Alerting was pinned to common-utils 3.7.0.0-SNAPSHOT via a TODO from an
earlier version bump. The DocRequest implementations that the
resource-sharing framework relies on landed on common-utils main
(3.8.0.0-SNAPSHOT), so tests that expect the security plugin to
intercept get/delete/index requests by resource id were seeing 200s
instead of 403s. Removing the pin and tracking opensearch_build keeps
alerting aligned with the OpenSearch line it is built against.

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

* Wire monitors and workflows into resource-sharing framework

Both resource types share `.opendistro-alerting-config`, so both are registered
as `ResourceProvider`s with `typeField = resource_type` — the security plugin
reads that top-level indexed field at shard-write time to route to the right
provider. Adds `resource_type` and `all_shared_principals` to the scheduled-jobs
mapping so the discriminator and DLS filter fields are indexed.

Async writes to internal indices go through `SdkClientExtensions.{put,get,delete}
DataObjectStashed` which stash the caller's transient auth per-call and restore
via `whenComplete` — mirrors ml-commons / flow-framework. Coroutine `.use { }`
around a suspend body doesn't survive resume-on-different-thread; per-call
stashing does.

Legacy `rbac_roles` validation in the monitor/workflow write actions is skipped
when RSC is active; the sharing entry is now the sole gate. The
`alerting_read_only` / `read_write` / `full_access` access levels are declared
for both `monitor` and `workflow` in `resource-access-levels.yml`.

`RSC_MIGRATION.md` documents the two-step upgrade path: alerting-side backfill
of `resource_type` and scratch owner fields via update-by-query, then the
security plugin's `POST /_plugins/_security/api/resources/migrate` seeds
sharing entries.

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

* Fix RSC IT suite: local helpers, revoke via PATCH, downgrade sequencing

The base test helpers (`updateMonitorWithClient` / `deleteMonitorWithClient`)
re-fetch through the admin client after the mutation, which 403s under RSC
because admin holds no share entry. Local `updateMonitorAs` / `deleteMonitorAs`
skip that re-fetch.

Other adjustments so the suite reflects framework semantics:
- Revoke uses `PATCH /_plugins/_security/api/resource/share` with a `revoke`
  body, not `POST /revoke` (which doesn't exist).
- The framework's `PUT /share` is add-only, not replace, so access-level
  downgrade needs an explicit revoke-then-share sequence.
- `test alerts inherit denial when monitor is not shared` accepts either 403
  (cluster-action gate rejects a user with no shares anywhere) or a 200 with
  empty results (DLS-filtered) — both satisfy the guarantee.
- User/role setup is idempotent across tests; @after no longer deletes users
  since config-cache reloads under repeated PUTs exposed a role-mapping race.
- `waitForResourceSharingEntry` polls the sharing index after monitor create
  because the security plugin records the entry asynchronously from postIndex.
- Two comment-flow tests marked `@Ignore` pending a stash inside
  `CommentsIndices.createOrUpdateInitialCommentsHistoryIndex` — non-owner
  calls throw an uncaught exception mid-coroutine and hang the response.

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

* Remove RSC_MIGRATION.md scratch design doc

Design belongs in the PR description or an internal wiki, not tracked in the
alerting source tree. Content moved out of band.

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

* Add POST /_plugins/_alerting/_migrate_to_rsc for RSC upgrade path

Admin-only endpoint that runs an update-by-query on `.opendistro-alerting-config`
to backfill the fields the security plugin's resource-sharing framework needs on
existing monitor and workflow docs:

  1. Top-level `resource_type` = "monitor" or "workflow", copied from the wrapper
     key. This is what `AlertingResourceSharingExtension`'s `ResourceProvider.typeField`
     points at, so postIndex on future writes can classify docs by type.
  2. Top-level `_migration_user_name` and `_migration_backend_roles`, copied from
     `<wrapper>.user.*`. These let the security plugin's
     `POST /_plugins/_security/api/resources/migrate` address monitors and workflows
     in a single call via `username_path: "/_migration_user_name"`.

The Painless script is idempotent — docs already carrying `resource_type` and
docs without either wrapper (metadata records) return `ctx.op = 'noop'`. The
query pre-filters via `must_not exists resource_type` so the noop branch only
runs on rare concurrent-write windows.

Response is a plain counters shape: `updated`, `noops`, `failures`, `took_millis`.
The transport action name `cluster:admin/opensearch/alerting/rsc/migrate` should
be granted only via `all_access` — not through the per-resource access levels.

Ships with an IT that seeds legacy-shape monitor/workflow/metadata/already-
migrated docs directly, hits the endpoint, and verifies each transitions
correctly (or noops).

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

* Purge legacy metadata docs during migrate + add E2E lifecycle IT

The security plugin's `POST /_plugins/_security/api/resources/migrate` scans the
entire source index and 400s if any doc's `resource_type` is null.
`<monitorId>-metadata` records aren't shareable resources, so alerting's migrate
now deletes them via delete-by-query on `metadata` field existence before the
resource_type backfill. Metadata regenerates on next monitor execution.

Adds `MigrateToRscE2ERestApiIT` covering the full lifecycle:
  phase 1: RSC disabled dynamically → alice creates a monitor via alerting REST,
    legacy backend-roles path works.
  phase 2: strip resource_type + sharing entry to simulate a truly-legacy doc.
  phase 3: enable RSC + protected_types → alice's GET returns 403 (no share).
  phase 4: call POST /_plugins/_alerting/_migrate_to_rsc, then
    POST /_plugins/_security/api/resources/migrate.
  phase 5: alice's GET returns 200 again — sharing entry now exists.

Also updates the metadata test in MigrateToRscRestApiIT to expect deletion
instead of untouched.

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

* JobSweeper: skip ancillary top-level fields when detecting job type

isSweepableJobType inspected only the very first FIELD_NAME to decide if a doc
was schedulable. Under resource-sharing, alerting docs are written with
`resource_type` (and possibly `all_shared_principals` / migration scratch
fields) at the top level before the `monitor`/`workflow` wrapper — so the
sweeper would silently skip newly-written jobs and they'd never be scheduled.
Iterate top-level fields until we find a sweepable type, skipping any extras.

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

* Drop top-level resource_type; classify via nested type paths

opensearch-project/security#6323 changes getResourceTypeForIndexOp to iterate
all matching ResourceProviders and pick the first whose typeField extraction
resolves. That lets us drop the redundant top-level `resource_type` field
and rely on the pre-existing nested `monitor.type` / `workflow.type` fields
as type discriminators.

Changes:
- AlertingResourceSharingExtension: typeField is now `monitor.type` and
  `workflow.type` per provider (previously both pointed at the shared
  `resource_type` field).
- TransportIndexMonitorAction / TransportIndexWorkflowAction: drop the
  `with_resource_type=true` XContent param from storage writes; the stored
  doc goes back to just `{"<wrapper>": {...}}`.
- scheduled-jobs.json mapping: drop the `resource_type` keyword field.
- TransportMigrateToRscAction: drop the resource_type backfill from the
  Painless script; keep the scratch owner-field copy (still needed by the
  security migrate endpoint's `username_path`). Idempotency filter now
  gates on `_migration_user_name` existence.
- Migrate ITs updated to match the new doc shape (no resource_type).

Depends on: opensearch-project/security#6323 (must land in opensearch_build
before local E2E ITs on the sharing race path will pass). Migrate UBQ tests
that don't touch the security plugin's classification path continue to pass.

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

* Drop _migrate_to_rsc endpoint; rely on security plugin's classifier

opensearch-project/security#6323 landed two framework capabilities that
render the alerting-side migration pre-processor obsolete:

  1. `ResourcePluginInfo.getResourceTypeForIndexOp` now iterates every
     registered provider on a shared index and picks the first one whose
     `typeField` extraction resolves — no top-level discriminator field
     needed on stored docs.
  2. `ResourceProvider` gained `ownerNamePath()` / `ownerBackendRolesPath()`
     methods. When declared, they override the request-level `username_path`
     / `backend_roles_path` on `POST /_plugins/_security/api/resources/migrate`
     for docs classified as that type — so a single migrate call attributes
     owners for both monitor and workflow docs sharing one index without any
     scratch fields.

Alerting cleanup:
- `AlertingResourceSharingExtension` declares per-type ownerNamePath and
  ownerBackendRolesPath (`/monitor/user/name`, `/workflow/user/name`, etc.).
- Delete `TransportMigrateToRscAction`, `RestMigrateToRscAction`,
  `MigrateToRscAction`/`Request`/`Response`, and unwire from `AlertingPlugin`
  — the endpoint no longer has a job to do.
- Delete the standalone `MigrateToRscRestApiIT` (unit tests for the deleted
  endpoint).
- Rename `MigrateToRscE2ERestApiIT` -> `RscMigrateE2ERestApiIT` and update it
  to call security's migrate endpoint directly. The security plugin now reads
  owner metadata from the per-provider paths so the request body no longer
  needs alerting-specific `_migration_*` scratch fields.

Verified locally against a security build carrying #6323: 1/1 E2E test
passes (create legacy monitor with RSC disabled → enable RSC → verify 403 →
run security migrate → verify alice recovers access).

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

* Post-review cleanups: extension tests cover workflow provider; drop stale comment

- AlertingResourceSharingExtensionTests: previous test asserted a single
  provider was registered. Now that the extension registers both monitor and
  workflow, replace that assertion and add per-provider coverage that pins
  typeField (`monitor.type` / `workflow.type`) and the per-type owner paths
  (`/monitor/user/name`, `/workflow/user/name`, and matching backend_roles
  paths). These are the contract downstream security PR #6323 reads from.

- JobSweeper.isSweepableJobType: drop mention of `resource_type` and the
  alerting migration scratch fields from the skip-loop comment. Neither is
  ever emitted after the resource_type revert; only security-injected
  top-level fields like `all_shared_principals` are relevant here.

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

* Accept 403|404 on post-delete GET; ignore flaky alerts-inherit test

Two test-expectation adjustments after running the RSC IT suite against the
merged upstream security build (opensearch-project/security#6323):

- 'test owner sees delete performed by read-write shared user': under RSC
  the sharing entry is removed alongside the doc, so alice's GET hits the
  RSC gate (403) before the transport action can return NOT_FOUND (404).
  Accept either — both semantically mean 'no longer accessible'.

- 'test alerts inherit access when monitor is shared read-only': bob has
  a read-only share on alice's monitor but the alerts GET returns an empty
  result. `getAccessibleResourceIds` correctly reports the monitor as
  accessible, so this looks like DLS on the alerts index filtering bob out
  even though the alerts index isn't itself a resource-sharing-protected
  type. Marked @ignore with a FIXME; needs a separate investigation and
  possibly an alerts-index DLS exemption, but doesn't block the core RSC
  framework onboarding.

Also add a refresh of `.opendistro-alerting-config-sharing` inside
`shareResource` so downstream `getAccessibleResourceIds` searches see the
newly-written sharing entry without an ad-hoc sleep.

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

* Fix schema-version test expectation and restore stash pattern for comments

- Bump AlertIndicesIT verifyIndexSchemaVersion expectations 8->9 to match
  scheduled-jobs.json mapping bump for all_shared_principals.
- Restore stashContext().use wrapper around comment-action coroutine launch
  to preserve prior behavior; per-call putDataObjectStashed retained for
  ResourceIndexListener.

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

* Unwrap CompletionException in SdkClient await so REST status survives

sdkClient.*Async().await() propagated the raw CompletionException that
CompletableFuture wraps around a failed stage's cause. AlertingException.wrap()
type-switches on the exception to derive the REST status and does not
recognize CompletionException, so it defaulted to 500 INTERNAL_SERVER_ERROR
and masked the real status -- e.g. a 409 CONFLICT from
VersionConflictEngineException on an optimistic-concurrency PUT.

Peel CompletionException/ExecutionException wrappers off the throwable
before resuming the coroutine so the original exception type (and its
status) reaches the wrap() call sites.

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

* Run RSC accessible-resource lookups under caller context; harden comment ITs

Under the resource-sharing framework, the alert/comment search actions filter
results to resources the caller can access via ResourceSharingClient
.getAccessibleResourceIds(). That call resolves the caller from the
authenticated-user header in ThreadContext, but the actions dispatch it from
inside a `stashContext().use { scope.launch { ... } }` block, so it ran under
the stashed (empty) context, saw no user, and returned no accessible
monitors/workflows -- yielding zero alerts/comments even for authorized users.
(Confirmed at runtime: transient user = null, accessibleMonitorIds = [].)

Capture the caller's context before stashing and restore it around only the
getAccessibleResourceIds call, keeping the subsequent system-index search under
the stashed plugin context. Applied to the three actions with this pattern:
- TransportSearchAlertingCommentAction
- TransportGetAlertsAction
- TransportGetWorkflowAlertsAction

Test changes (SecureAlertingCommentsRestApiIT + AlertingRestTestCase):
- Add shareMonitorWithUser / waitForResourceSharingEntry helpers to the base
  class. Viewing a monitor's comments is a read gated by resource authz, so the
  three "can view comments" tests now share the admin-created monitor with the
  viewing user; waitForResourceSharingEntry avoids racing the security plugin's
  async postIndex sharing-entry write.
- Tear down the shared/reserved role-mappings in @after so a test that fails
  before its own cleanup can't leak a mapping and grant a later test's user
  unexpected access (order-dependent flakiness).
- Drop the now-duplicate private waitForResourceSharingEntry in
  SecureResourceSharingMonitorRestApiIT in favor of the base-class helper.

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

* Initialize alerts index mapping in comment ITs so RSC monitor_id filter matches

SecureAlertingCommentsRestApiIT creates alerts via the raw _doc API without
first initializing the alerts index. The index then auto-creates with a dynamic
`text` mapping for monitor_id (plus a .keyword subfield), so the RSC comment
search filter's `termsQuery("monitor_id", ...)` on the analyzed field matched
nothing -- a monitor shared with the viewing user returned zero comments.

Call putAlertMappings() in @before (as MonitorRestApiIT already does for the
same "no create alert API" reason) so the alerts index uses the real mapping
where monitor_id is keyword and the term filter matches. Verified: the full
SecureAlertingCommentsRestApiIT suite passes (14/0) against fixed common-utils
and security deps.

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

* Rename workflow RSC resource type to alerting-workflow to avoid flow-framework collision

flow-framework already registers a resource type named "workflow" in the shared
resource-sharing registry, so alerting must use a distinct identifier. Rename the
resource type (not the stored doc wrapper key, JSON paths, or transport action
names) from "workflow" to "alerting-workflow":

- ResourceSharingUtils.WORKFLOW_RESOURCE_TYPE constant (propagates to all
  workflow transport actions and AlertingResourceSharingExtension.resourceType())
- resource-access-levels.yml top-level type key
- integTest protected_types cluster setting
- extension unit-test assertions and the RSC migrate E2E test's protected_types
  and default_access_level map key

typeField()/ownerNamePath() ("/workflow/...") and the cluster:*/workflow/* action
names are unchanged -- they reference the stored ScheduledJob doc structure and
action registry, not the resource-sharing type.

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

* Run workflow delete on plugin subject; accept 403|404 on post-delete GET

Two fixes surfaced once CI could finally compile (security-spi snapshot caught
up with #6323), which exposed the security-enabled WorkflowRestApiIT delete tests:

1. TransportDeleteWorkflowAction restored the caller's ThreadContext in the
   delete coroutine, making the handler's config-index reads/deletes run as the
   caller. With resource sharing OFF the caller can't touch that system index
   (spurious 404 "Workflow not found"); with it ON a caller read races the async
   share-entry write. The restore was unnecessary: the security plugin's
   ResourceIndexListener.postDelete cleans up the share entry by resource id only
   and never reads the caller. Drop the restore so the handler runs on the plugin
   subject (as it did before RSC onboarding); postDelete cleanup still fires on
   the shard-level delete regardless of initiator.

2. Under resource sharing, GET-ing a just-deleted workflow/monitor returns 403
   (the RSC gate denies once the sharing entry is gone) rather than 404. The
   post-delete verification in the three delete-workflow tests now accepts either
   via a shared assertDeletedNotAccessible helper -- both mean "no longer
   accessible" -- mirroring the pattern already used in the secure monitor ITs.

Verified locally against fixed common-utils + security deps: all three delete
tests pass in both the plain security and resource-sharing-enabled variants.

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

* Scope RSC security CI variant to resource-sharing suites only

The security workflow ran the full IT suite (--tests '*IT') under both the
resource-sharing-disabled and -enabled variants. The backend-role / filterByAccessStrategy
secure suites (e.g. SecureMonitorRestApiIT) and the general functional ITs assume the
pre-resource-sharing access model, so they fail spuriously under RSC (monitor GET/HEAD/DELETE
now requires a sharing entry rather than a backend role). Resource-sharing access is covered by
its own dedicated suites.

Run the pre-RSC/functional suites only when resource sharing is OFF, and run only the
resource-sharing-specific suites (SecureResourceSharingMonitorRestApiIT, RscMigrateE2ERestApiIT
-- both already gated on the feature via assumeTrue) when it is ON.

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

* Address RSC review: super-admin bypass, alert-search size, rbac_roles, comments stash

Review feedback on #2180 (riysaxen-amzn):

- Super-admin visibility: the RSC filter branch preceded the `user == null`
  (super-admin) branch in the alerts/workflow-alerts/comment-search/destinations
  read paths, so a super-admin got filtered to only shared resources. Check
  `user == null` first so super-admin (and the security-disabled case) sees
  everything even under resource sharing.

- Bug: getAccessibleAlertIDs (and the legacy getFilteredAlertIDs) never set a
  search size, capping alert resolution at the default 10 and silently dropping
  comments for alerts beyond the first 10. Set size to MAX_SEARCH_SIZE.

- rbac_roles hardening: validation was skipped under RSC. Since the feature flag
  is dynamic, validate caller-supplied rbac_roles regardless of RSC so a
  non-admin can't persist roles they don't hold that would gate access if RSC is
  later disabled.

- Comments-history index bootstrap now runs on the plugin subject (stashed in
  the comment index action's start()); previously a non-admin caller's
  indices().exists() threw under RSC and the request hung.

- Destinations: super-admin now runs a direct (non-DLS) search so it isn't
  filtered by the resource-sharing DLS path.

- Document the index.max_terms_count bound at the monitor/workflow-id term
  filters; derive the sharing index name from the config index constant in test
  helpers rather than hardcoding.

Subordinate-resource alert/comment access tests remain @ignore'd pending the
child-resource sharing model in opensearch-project/security#6373 (updated the
FIXMEs to reference it). Verified SecureResourceSharingMonitorRestApiIT: 31 tests,
3 skipped, 0 failures under the resource-sharing variant.

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

* Trigger CI: pull common-utils 3.8 snapshot after backport of #984 + #981

Common-utils 3.8 now includes the order-independent ScheduledJob.parse
(that tolerates security's ancillary all_shared_principals field) and
the DocRequest.type() overrides on alerting request classes (so the
security plugin's ResourceAccessEvaluator actually gates transport
GET/DELETE/etc. under RSC).

Empty commit to force the fresh 3.8.0.0-SNAPSHOT to be pulled on the
next CI run.

Refs: opensearch-project/common-utils#998
Signed-off-by: Darshit Chanpura <dchanp@amazon.com>

* Enforce RSC on subordinate alerts/comments; enable inheritance tests

security#6373 (child-resource sharing) merged, so the three subordinate-
resource tests no longer need to be ignored. Enabling them surfaced two real
gaps that are now fixed:

- Alert GET inheritance: GetAlertsRequest already reports type()=monitor /
  id()=monitorId, so the security ResourceAccessEvaluator gates a by-monitor
  alerts GET as a monitor access check (a read-only share grants it, no share
  is 403 before the transport action runs). But TransportGetAlertsAction then
  executed the alert-index search under the *caller's* restored context, so a
  read-only-shared, non-owner caller (no direct perms on the system alerts
  index) got a 500. Run the search on the plugin subject instead; the
  monitor_id filter, bounded to the caller's accessible monitors, is the
  resource-sharing boundary.

- Comment create: the comment request targets the comments index (not the
  monitor) so the evaluator does not gate it, and the transport action skipped
  its own check under RSC — any caller could comment on any alert. Gate the
  create on the caller's access to the parent monitor via
  ResourceSharingClient.verifyAccess with the comment write action (granted
  only at read-write / full-access). Comment delete already restricts to the
  author/admin, so it needs no change.

Un-ignores 'test alerts inherit access when monitor is shared read-only',
'test comment on alert denied without share', and 'test comment on alert
allowed with read-write share'. Full SecureResourceSharingMonitorRestApiIT
suite: 31 passed, 0 skipped, 0 failed under -Dresource_sharing.enabled=true.

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

---------

Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
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.

2 participants