Skip to content

Override DocRequest.type() on alerting request classes; tolerate ancillary top-level fields in ScheduledJob.parse#981

Merged
cwperks merged 10 commits into
opensearch-project:mainfrom
DarshitChanpura:alerting-docrequest-type
Jul 24, 2026
Merged

Override DocRequest.type() on alerting request classes; tolerate ancillary top-level fields in ScheduledJob.parse#981
cwperks merged 10 commits into
opensearch-project:mainfrom
DarshitChanpura:alerting-docrequest-type

Conversation

@DarshitChanpura

@DarshitChanpura DarshitChanpura commented Jul 22, 2026

Copy link
Copy Markdown
Member

Summary

Two related changes required by alerting's onboarding to the security plugin's resource-sharing framework (RSC), driven by opensearch-project/alerting#2180 and opensearch-project/security#6323.

1. DocRequest.type() overrides

The security plugin's ResourceAccessEvaluator gates a request only when DocRequest.type() is contained in the configured protected_types list. The default type() returns \"indices\", so without an override the evaluator skipped every alerting request and the ActionFilter granted access unconditionally.

Follow-up to #980 which added DocRequest but did not override type().

  • Add MONITOR_RESOURCE_TYPE and WORKFLOW_RESOURCE_TYPE constants to AlertingConstants.
  • Monitor-scoped request classes (IndexMonitorRequest, DeleteMonitorRequest, GetMonitorRequest, AcknowledgeAlertRequest, AcknowledgeChainedAlertRequest, GetAlertsRequest, GetFindingsRequest, DocLevelMonitorFanOutRequest) return \"monitor\".
  • Workflow-scoped request classes (IndexWorkflowRequest, DeleteWorkflowRequest, GetWorkflowRequest, GetWorkflowAlertsRequest) return \"workflow\".
  • Comment requests unchanged — comments are not a protected resource type.

2. ScheduledJob.parse tolerates ancillary top-level fields

When a doc is stored on a resource-sharing-protected index, the security plugin's postIndex hook writes all_shared_principals as a top-level field on the doc for DLS filtering. Legacy stored docs may also carry other top-level fields written by admin migration tools. The old parse implementation assumed the doc had a single wrapper ({\"monitor\": {...}} or {\"workflow\": {...}}) and threw on any additional top-level fields.

Both parse overloads now iterate all top-level fields, dispatch to the registered subtype parser (Monitor.parse / Workflow.parse) when the field name matches a known wrapper, and skip any other top-level fields via skipChildren().

Backward compatibility

  • Old-shape docs ({\"monitor\": {...}}) parse identically — the wrapper is still the only recognized field.
  • New-shape docs ({\"monitor\": {...}, \"all_shared_principals\": [...]}) now parse instead of throwing.
  • No changes to Monitor/Workflow XContent output — the write shape is unchanged, so downstream consumers see the same docs they always have.

Related

The security plugin's ResourceAccessEvaluator gates a request only when
DocRequest.type() matches one of the configured protected_types. The
default implementation returns "indices", so without an override the
evaluator skipped every alerting request and the ActionFilter granted
access unconditionally.

Override type() on every alerting DocRequest that targets the shared
resource index (SCHEDULED_JOBS_INDEX). Adds a MONITOR_RESOURCE_TYPE
constant in AlertingConstants so the value has a single source of
truth. Comment requests do not need this since comments are not a
registered resource type.

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

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit cb17148)

Here are some key observations to aid the review process:

🧪 No relevant tests
🔒 No security concerns identified
✅ No TODO sections
🔀 Multiple PR themes

Sub-PR theme: Override DocRequest.type() on alerting request classes

Relevant files:

  • src/main/kotlin/org/opensearch/commons/alerting/action/AcknowledgeAlertRequest.kt
  • src/main/kotlin/org/opensearch/commons/alerting/action/AcknowledgeChainedAlertRequest.kt
  • src/main/kotlin/org/opensearch/commons/alerting/action/DeleteMonitorRequest.kt
  • src/main/kotlin/org/opensearch/commons/alerting/action/DeleteWorkflowRequest.kt
  • src/main/kotlin/org/opensearch/commons/alerting/action/DocLevelMonitorFanOutRequest.kt
  • src/main/kotlin/org/opensearch/commons/alerting/action/GetAlertsRequest.kt
  • src/main/kotlin/org/opensearch/commons/alerting/action/GetFindingsRequest.kt
  • src/main/kotlin/org/opensearch/commons/alerting/action/GetMonitorRequest.kt
  • src/main/kotlin/org/opensearch/commons/alerting/action/GetWorkflowAlertsRequest.kt
  • src/main/kotlin/org/opensearch/commons/alerting/action/GetWorkflowRequest.kt
  • src/main/kotlin/org/opensearch/commons/alerting/action/IndexMonitorRequest.kt
  • src/main/kotlin/org/opensearch/commons/alerting/action/IndexWorkflowRequest.kt
  • src/main/kotlin/org/opensearch/commons/alerting/util/AlertingConstants.kt

Sub-PR theme: Tolerate ancillary top-level fields in ScheduledJob.parse

Relevant files:

  • src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt

⚡ Recommended focus areas for review

Incorrect resource type

AcknowledgeChainedAlertRequest operates on a workflow (its id() returns workflowId) but type() returns MONITOR_RESOURCE_TYPE. This mismatch means the security plugin's ResourceAccessEvaluator will evaluate access against the monitor resource type rather than workflow, likely causing incorrect authorization decisions for chained-alert acknowledgement. It should return WORKFLOW_RESOURCE_TYPE for consistency with the other workflow-scoped requests.

override fun type(): String {
    return AlertingConstants.MONITOR_RESOURCE_TYPE
}
Possible parser state bug

After xcp.namedObject(...) consumes the wrapper object, the parser is positioned at that inner END_OBJECT. The loop then calls xcp.nextToken() which advances to the next field or the outer END_OBJECT — this is correct. However, when the current field is a non-wrapper scalar (e.g. all_shared_principals as a string/number), xcp.skipChildren() on a scalar VALUE token is a no-op and the parser remains on the value; the subsequent xcp.nextToken() correctly moves on. This appears to work for scalars/objects/arrays, but confirm behavior for all field value types that legacy migration tools may write, since a non-advancing skip on some token types would cause an infinite loop or malformed-token error.

private fun parseWrapper(xcp: XContentParser, expectedType: String?, id: String, version: Long): ScheduledJob {
    XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp)
    var job: ScheduledJob? = null
    var token = xcp.nextToken()
    while (token != null && token != XContentParser.Token.END_OBJECT) {
        XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, xcp)
        val fieldName = xcp.currentName()
        xcp.nextToken() // advance to value
        val isWrapper = fieldName in SCHEDULED_JOB_WRAPPER_FIELDS &&
            (expectedType == null || fieldName == expectedType)
        if (isWrapper && job == null) {
            XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp)
            job = xcp.namedObject(ScheduledJob::class.java, fieldName, null)
        } else {
            xcp.skipChildren()
        }
        token = xcp.nextToken()
    }
    requireNotNull(job) { "No recognized ScheduledJob subtype wrapper found in document" }
    XContentParserUtils.ensureExpectedToken(XContentParser.Token.END_OBJECT, token, xcp)
    return job.fromDocument(id, version)

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to cb17148

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Fail fast on wrapper type mismatch

When expectedType is provided but the field name matches a known wrapper key that
differs from expectedType, silently skipping it can mask a real type mismatch and
yield a misleading "No recognized ScheduledJob subtype wrapper found" error.
Consider throwing an explicit error when a wrapper key is found but does not match
the expected type, to preserve the original contract of the typed parse overload.

src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt [81-86]

 if (isWrapper && job == null) {
     XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp)
     job = xcp.namedObject(ScheduledJob::class.java, fieldName, null)
+} else if (fieldName in SCHEDULED_JOB_WRAPPER_FIELDS && expectedType != null && fieldName != expectedType) {
+    throw IllegalArgumentException("Expected ScheduledJob wrapper '$expectedType' but found '$fieldName'")
 } else {
     xcp.skipChildren()
 }
Suggestion importance[1-10]: 6

__

Why: Reasonable suggestion: preserving the original contract of the typed parse overload by failing on wrapper mismatch improves error clarity and avoids masking bugs. Moderate impact.

Low
Guard against premature end of stream

If the outer object is empty or truncated, xcp.nextToken() may return null, and the
loop will exit with job == null producing an unhelpful message; also the inner
xcp.nextToken() result is discarded, so a stream that ends unexpectedly after the
field name would advance to null and then ensureExpectedToken(START_OBJECT, ...) on
a null current token could NPE. Consider capturing and validating the value token
explicitly.

src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt [74-78]

 var token = xcp.nextToken()
 while (token != null && token != XContentParser.Token.END_OBJECT) {
     XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, xcp)
     val fieldName = xcp.currentName()
-    xcp.nextToken() // advance to value
+    val valueToken = xcp.nextToken() ?: throw IOException("Unexpected end of input after field '$fieldName'")
Suggestion importance[1-10]: 4

__

Why: Minor defensive improvement; XContentParser typically handles malformed input, but explicit validation provides clearer error messages. Low-to-moderate impact.

Low

Previous suggestions

Suggestions up to commit 138ac72
CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix trailing-field skip loop token handling

In this overload, namedObject is invoked without first consuming the wrapper field's
START_OBJECT token, which differs from the no-arg overload and the previous behavior
(which called xcp.nextToken() before validating END_OBJECT). The caller here is
expected to have already positioned at the wrapper field name, so namedObject may
leave the parser inside the wrapper object; the subsequent skip loop will then
consume the wrapper's inner fields rather than sibling top-level fields, potentially
misreading the document. Ensure the parser is advanced past the wrapper's END_OBJECT
before scanning for trailing top-level fields.

src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt [74-86]

 XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp)
 val job = xcp.namedObject(ScheduledJob::class.java, type, null)
-// Skip any trailing top-level fields that live after the wrapper (for example the
-// security plugin's `all_shared_principals` DLS field) — see the no-arg overload above.
+// After namedObject returns, advance to the next token at the outer level and skip
+// any trailing top-level fields (for example the security plugin's
+// `all_shared_principals` DLS field) — see the no-arg overload above.
 var next = xcp.nextToken()
 while (next != null && next != XContentParser.Token.END_OBJECT) {
-    if (next == XContentParser.Token.FIELD_NAME) {
-        xcp.nextToken()
-        xcp.skipChildren()
-    }
+    XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, next, xcp)
+    xcp.nextToken()
+    xcp.skipChildren()
     next = xcp.nextToken()
 }
 XContentParserUtils.ensureExpectedToken(XContentParser.Token.END_OBJECT, next, xcp)
Suggestion importance[1-10]: 6

__

Why: The suggestion raises a legitimate concern about parser state after namedObject returns and improves the loop by properly validating FIELD_NAME tokens. However, namedObject typically consumes the entire object including END_OBJECT, so the original code may be functionally correct; the improvement is mainly about stricter validation.

Low
General
Avoid hardcoding ScheduledJob subtype names

The wrapper allow-list SCHEDULED_JOB_WRAPPER_FIELDS is hardcoded to {"monitor",
"workflow"}, but the surrounding framework uses XContentParser.namedObject for
extensibility — third-party ScheduledJob subtypes registered via the XContent
registry will now be silently skipped rather than parsed. Consider either
dispatching to namedObject for any field whose value is an object (letting the
registry decide) or exposing a way to extend the wrapper set, so custom job types
continue to work.

src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt [49-60]

 while (token != null && token != XContentParser.Token.END_OBJECT) {
     XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, xcp)
     val fieldName = xcp.currentName()
-    xcp.nextToken() // advance to value
-    if (fieldName in SCHEDULED_JOB_WRAPPER_FIELDS && job == null) {
-        XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp)
-        job = xcp.namedObject(ScheduledJob::class.java, fieldName, null)
+    val valueToken = xcp.nextToken() // advance to value
+    if (job == null && valueToken == XContentParser.Token.START_OBJECT) {
+        try {
+            job = xcp.namedObject(ScheduledJob::class.java, fieldName, null)
+        } catch (e: Exception) {
+            xcp.skipChildren()
+        }
     } else {
         xcp.skipChildren()
     }
     token = xcp.nextToken()
 }
Suggestion importance[1-10]: 5

__

Why: Valid point about extensibility — hardcoding wrapper names could break third-party ScheduledJob subtypes registered via the XContent registry. However, using try/catch on namedObject as a control flow mechanism is fragile, and the PR author may have intentionally limited the allow-list for safety.

Low
Suggestions up to commit 963e609
CategorySuggestion                                                                                                                                    Impact
Possible issue
Advance parser into wrapper before namedObject

The typed parse overload calls namedObject on the outer START_OBJECT token without
first advancing into the wrapper's inner object, which is inconsistent with the
no-arg overload and will likely fail to parse the wrapped payload. Advance to
FIELD_NAME and inner START_OBJECT before invoking namedObject, similar to the
previous implementation.

src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt [76-89]

+XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp)
+XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, xcp.nextToken(), xcp)
 XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp)
 val job = xcp.namedObject(ScheduledJob::class.java, type, null)
-// Skip any trailing top-level fields that live after the wrapper — see the no-arg
-// overload above for the same rationale (RSC discriminator, DLS injection, migration
-// scratch fields).
 var next = xcp.nextToken()
 while (next != null && next != XContentParser.Token.END_OBJECT) {
     if (next == XContentParser.Token.FIELD_NAME) {
         xcp.nextToken()
         xcp.skipChildren()
     }
     next = xcp.nextToken()
 }
 XContentParserUtils.ensureExpectedToken(XContentParser.Token.END_OBJECT, next, xcp)
Suggestion importance[1-10]: 9

__

Why: This appears to be a real bug: the typed parse overload no longer advances to the FIELD_NAME and inner START_OBJECT before calling namedObject, which is inconsistent with the previous behavior and will likely break parsing when this overload is used.

High
General
Avoid hardcoding recognized wrapper field names

Hardcoding SCHEDULED_JOB_WRAPPER_FIELDS to {"monitor", "workflow"} couples this
class to a fixed set of subtypes, defeating the extensibility promised by the
namedObject registry. If new ScheduledJob subtypes are registered later, this parser
will silently skip them. Consider driving recognition off the registry or a
pluggable set instead of hardcoding.

src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt [51-62]

 while (token != null && token != XContentParser.Token.END_OBJECT) {
     XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, xcp)
     val fieldName = xcp.currentName()
     xcp.nextToken() // advance to value
-    if (fieldName in SCHEDULED_JOB_WRAPPER_FIELDS && job == null) {
-        XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp)
-        job = xcp.namedObject(ScheduledJob::class.java, fieldName, null)
+    if (job == null && xcp.currentToken() == XContentParser.Token.START_OBJECT) {
+        try {
+            job = xcp.namedObject(ScheduledJob::class.java, fieldName, null)
+        } catch (e: Exception) {
+            xcp.skipChildren()
+        }
     } else {
         xcp.skipChildren()
     }
     token = xcp.nextToken()
 }
Suggestion importance[1-10]: 4

__

Why: Valid design concern about extensibility, but the improved code uses try/catch on exceptions which is a fragile pattern, and the current hardcoded set is a deliberate whitelist to safely ignore injected fields.

Low
Suggestions up to commit e5b4e18
CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix token advancement in trailing-field skip loop

After xcp.skipChildren() on a value token, calling xcp.nextToken() at the loop
bottom advances past the next field name, causing the following iteration's
FIELD_NAME check to be skipped or misaligned. Restructure the loop so that after
skipping a value, the next iteration reads the following FIELD_NAME cleanly, or
simply check the token in a single-branch loop that always advances once per field.

src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt [84-92]

 var next = xcp.nextToken()
 while (next != null && next != XContentParser.Token.END_OBJECT) {
-    if (next == XContentParser.Token.FIELD_NAME) {
-        xcp.nextToken()
-        xcp.skipChildren()
-    }
+    XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, next, xcp)
+    xcp.nextToken()
+    xcp.skipChildren()
     next = xcp.nextToken()
 }
 XContentParserUtils.ensureExpectedToken(XContentParser.Token.END_OBJECT, next, xcp)
Suggestion importance[1-10]: 6

__

Why: The original loop's logic does appear to double-advance tokens after skipping a value (calling nextToken at bottom after skipChildren), which could misalign parsing of subsequent fields. The suggested restructuring is cleaner and more correct, though the actual impact depends on how skipChildren positions the parser.

Low
General
Avoid hardcoded wrapper field allow-list

SCHEDULED_JOB_WRAPPER_FIELDS is hardcoded to {"monitor", "workflow"}, but the parser
is designed around XContentParser.namedObject which discovers subtypes registered in
the xcontent registry at runtime. Hardcoding the set defeats extensibility — any
future ScheduledJob subtype registered externally will be silently skipped rather
than parsed. Consider making the wrapper detection dynamic (e.g., attempt
namedObject and fall back to skipChildren on failure) or otherwise decoupled from a
static allow-list.

src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt [52-65]

 var job: ScheduledJob? = null
 var token = xcp.nextToken()
 while (token != null && token != XContentParser.Token.END_OBJECT) {
     XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, xcp)
     val fieldName = xcp.currentName()
     xcp.nextToken() // advance to value
-    if (fieldName in SCHEDULED_JOB_WRAPPER_FIELDS && job == null) {
-        XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp)
-        job = xcp.namedObject(ScheduledJob::class.java, fieldName, null)
+    if (job == null && xcp.currentToken() == XContentParser.Token.START_OBJECT) {
+        try {
+            job = xcp.namedObject(ScheduledJob::class.java, fieldName, null)
+        } catch (_: Exception) {
+            xcp.skipChildren()
+        }
     } else {
         xcp.skipChildren()
     }
     token = xcp.nextToken()
 }
Suggestion importance[1-10]: 4

__

Why: The point about extensibility is valid — hardcoding {"monitor", "workflow"} limits future subtype registration. However, using try/catch on namedObject as a control flow mechanism is a debatable trade-off, and the current codebase only has these two known subtypes.

Low
Suggestions up to commit 72d56dc
CategorySuggestion                                                                                                                                    Impact
Possible issue
Harden ScheduledJob parser loop termination

The loop condition token != null && token != END_OBJECT may allow the parser to
iterate past the intended document boundary if nextToken() returns null (end of
stream) without hitting END_OBJECT, silently accepting malformed input. Also, using
SCHEDULED_JOB_WRAPPER_FIELDS as a hard-coded set will break when new ScheduledJob
subtypes are registered via the plugin's NamedXContentRegistry. Consider dispatching
based on registry lookup (e.g., try namedObject and fall back to skipChildren on
failure) or at minimum fail fast on unexpected EOF.

src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt [52-67]

 var job: ScheduledJob? = null
 var token = xcp.nextToken()
-while (token != null && token != XContentParser.Token.END_OBJECT) {
-    XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, xcp)
+while (token == XContentParser.Token.FIELD_NAME) {
     val fieldName = xcp.currentName()
     xcp.nextToken() // advance to value
     if (fieldName in SCHEDULED_JOB_WRAPPER_FIELDS && job == null) {
         XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp)
         job = xcp.namedObject(ScheduledJob::class.java, fieldName, null)
     } else {
         xcp.skipChildren()
     }
     token = xcp.nextToken()
 }
 requireNotNull(job) { "No recognized ScheduledJob subtype wrapper found in document" }
 XContentParserUtils.ensureExpectedToken(XContentParser.Token.END_OBJECT, token, xcp)
Suggestion importance[1-10]: 5

__

Why: The suggestion offers a minor robustness improvement by tightening the loop condition to token == FIELD_NAME, which would fail-fast on unexpected EOF. However, the original code's behavior is similar since ensureExpectedToken catches non-null non-END_OBJECT non-FIELD_NAME cases, and the hard-coded wrapper set concern is valid but not addressed in the improved code.

Low
Suggestions up to commit 399e819
CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix trailing-field skip loop in parser

After namedObject consumes the inner object, the parser is positioned at the inner
END_OBJECT, not past it. The next token could be either an END_OBJECT (outer
wrapper) or a FIELD_NAME (trailing injected field). The current loop calls
skipChildren() on non-object tokens like FIELD_NAME which does nothing, causing an
infinite-loop risk or improper skipping. Advance to the field's value before
skipping, and only skip when positioned on a value/START_OBJECT.

src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt [56-64]

 XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp)
 val job = xcp.namedObject(ScheduledJob::class.java, xcp.currentName(), null)
-// The security plugin may inject additional top-level fields (e.g. `all_shared_principals`
-// for DLS on resource-sharing indices) that live alongside the type wrapper. Skip through
-// any trailing fields until we hit the outer END_OBJECT.
+// Skip any trailing top-level fields injected by other plugins (e.g. security resource-sharing).
 var next = xcp.nextToken()
 while (next != null && next != XContentParser.Token.END_OBJECT) {
-    xcp.skipChildren()
+    if (next == XContentParser.Token.FIELD_NAME) {
+        xcp.nextToken()
+        xcp.skipChildren()
+    }
     next = xcp.nextToken()
 }
 XContentParserUtils.ensureExpectedToken(XContentParser.Token.END_OBJECT, next, xcp)
Suggestion importance[1-10]: 7

__

Why: Valid concern: calling skipChildren() when positioned on a FIELD_NAME token is a no-op in XContentParser, so trailing injected fields' values may not be properly skipped. The fix advances to the value before skipping, improving robustness.

Medium
General
Handle leading injected fields more robustly

The parser assumes resource_type appears only as the first field, but the security
plugin may inject arbitrary top-level fields in any position. To be robust and
consistent with the trailing-fields handling, loop over unknown top-level fields
until the type-wrapper field is found rather than only special-casing a leading
resource_type.

src/main/kotlin/org/opensearch/commons/alerting/model/ScheduledJob.kt [47-53]

 XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp)
-XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, xcp.nextToken(), xcp)
-// Skip the optional resource_type discriminator field that lives at the top level alongside
-// the type wrapper. Older docs (pre resource-sharing) did not emit it; newer docs do.
-if (xcp.currentName() == RESOURCE_TYPE_FIELD) {
-    xcp.nextToken() // -> VALUE_STRING
-    xcp.text() // consume value
-    XContentParserUtils.ensureExpectedToken(XContentParser.Token.FIELD_NAME, xcp.nextToken(), xcp)
+var token = xcp.nextToken()
+while (token == XContentParser.Token.FIELD_NAME) {
+    val fieldName = xcp.currentName()
+    val valueToken = xcp.nextToken()
+    if (valueToken == XContentParser.Token.START_OBJECT) {
+        // Reached the type wrapper; break out to let namedObject handle it.
+        break
+    }
+    // Consume scalar/array top-level fields (e.g. resource_type, security-injected metadata).
+    xcp.skipChildren()
+    token = xcp.nextToken()
 }
Suggestion importance[1-10]: 5

__

Why: Reasonable robustness improvement to handle arbitrary leading injected fields rather than only resource_type, but this is speculative since the current PR specifically targets the known resource_type discriminator.

Low

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

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit d47c2a5

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

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 0461839

}

override fun type(): String {
return AlertingConstants.MONITOR_RESOURCE_TYPE

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is workflow not a distinct type?

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

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit ba1899d

builder.startObject()
if (params.paramAsBoolean("with_type", false)) builder.startObject(type)
if (params.paramAsBoolean("with_type", false)) {
builder.field(ScheduledJob.RESOURCE_TYPE_FIELD, type)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be breaking?

builder.startObject()
if (params.paramAsBoolean("with_type", false)) builder.startObject(type)
if (params.paramAsBoolean("with_type", false)) {
builder.field(ScheduledJob.RESOURCE_TYPE_FIELD, type)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

…s in parse

- Separate `with_resource_type` param from `with_type`; only storage writes
  emit the top-level `resource_type` discriminator. Keeps historical
  `with_type=true` XContent shape unchanged so cross-plugin/cross-version
  parsers aren't affected.
- ScheduledJob.parse skips trailing top-level fields (e.g. the security
  plugin's `all_shared_principals` DLS field injected on resource-sharing
  indices) so it reads both new and post-share docs cleanly.

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

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 399e819

The security plugin's resource-sharing framework and the alerting migration
endpoint both inject top-level fields alongside the wrapper — `resource_type`,
`all_shared_principals`, `_migration_user_name`, `_migration_backend_roles`.
Rewrite `parse` to iterate all top-level fields, dispatch to the registered
subtype parser when the field is a known wrapper (`monitor` / `workflow`), and
skip everything else via `skipChildren`. Fixes parse failures like:

  expecting token of type [START_OBJECT] but found [START_ARRAY]

when a doc carries `_migration_backend_roles: [...]` at the top level.

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

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 72d56dc

The 2-arg overload used by JobSweeper (when the type has already been
identified) expected outer END_OBJECT immediately after the wrapper. Docs
carrying `all_shared_principals` or the alerting migration scratch fields
(`_migration_user_name`, `_migration_backend_roles`) trip that assumption.
Mirror the no-arg overload's skip-until-END_OBJECT loop.

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

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit e5b4e18

opensearch-project/security#6323 lets ResourceProvider.typeField be a
type-specific nested path (`monitor.type` / `workflow.type`) instead of
requiring a single shared top-level discriminator. So alerting no longer
needs to emit `resource_type` on write, and common-utils no longer needs
to expose that field or param.

Kept: the tolerant top-level field iteration in ScheduledJob.parse. The
security plugin's DLS injection (`all_shared_principals`) and the alerting
migration endpoint's scratch owner fields still land alongside the wrapper,
so the parser must still skip unknown top-level fields.

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

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 963e609

}
next = xcp.nextToken()
}
XContentParserUtils.ensureExpectedToken(XContentParser.Token.END_OBJECT, next, xcp)

@DarshitChanpura DarshitChanpura Jul 23, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these changes needed?

What the file is for

.opendistro-alerting-config is a shared filing cabinet. Monitors and workflows both live there. Each doc looks like a folder with one big envelope inside:

  • Monitor doc: { "monitor": { …monitor fields… } }
  • Workflow doc: { "workflow": { …workflow fields… } }

ScheduledJob.parse is the mail-opener. Given one of these docs, it opens the outer folder, looks at the envelope's name (monitor or workflow), and hands the envelope's contents to the matching parser (Monitor.parse or Workflow.parse).

What the old parser did

The old code was hard-coded to a rigid procedure:

  1. Open the outer folder ({)
  2. Expect exactly ONE thing inside — the envelope
  3. Read the envelope's name
  4. Open the envelope, hand contents to the specialist
  5. Expect the outer folder to close (}) with nothing else inside

If it found anything other than the single envelope — for example, a sticky note stuck to the inside of the folder — it errored out.

Why that broke

When resource-sharing is on, the security plugin stamps every doc with an all_shared_principals: [...] field for DLS. That's a sticky note next to the envelope. The old parser saw two things in the folder and threw.

What the new parser does

Both overloads (parse(xcp, id, version) and parse(xcp, type, id, version)) now delegate to a single private helper parseWrapper. It reads every item inside the outer folder:

  • If the item's name is monitor or workflow (defined in SCHEDULED_JOB_WRAPPER_FIELDS), that's the envelope — open it and hand off to the specialist. The 2-arg overload additionally requires the name to match the caller-supplied type.
  • Anything else (e.g. all_shared_principals) — it's a sticky note, skip via xcp.skipChildren().

At the end it verifies an envelope was found (requireNotNull(job)) and that the outer folder closed properly.

The shared helper is order-independent — a sticky note can appear before or after the envelope; the wrapper still gets picked up.

@DarshitChanpura DarshitChanpura changed the title Override DocRequest.type() on alerting request classes Override DocRequest.type() on alerting request classes; tolerate ancillary top-level fields in ScheduledJob.parse Jul 23, 2026
* expected to be of the form:
* { "<job_type>" : { <job fields> } }
* Any additional top-level fields (e.g. `all_shared_principals` injected by the security
* plugin's resource-sharing framework, or migration scratch fields) are tolerated and

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we update this comment if those "scratch fields" were coming from some previous WIP code?

Migration is handled upstream by the security plugin now (per-provider owner
paths on ResourceProvider), so alerting no longer emits _migration_* scratch
fields. Simplify the skip-loop comments to name only what the security plugin
actually injects at index time (all_shared_principals for DLS).

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

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 138ac72

@@ -51,7 +75,18 @@ interface ScheduledJob : BaseModel {
fun parse(xcp: XContentParser, type: String, id: String = NO_ID, version: Long = NO_VERSION): ScheduledJob {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an opportunity for code re-use here if the purpose is similar to the block above? I'm not sure if this is robust to ordering of keys.

Both parse overloads now delegate to a private parseWrapper helper. This
fixes an order-dependence bug in the 2-arg overload: previously it called
namedObject at the very first non-outer-START_OBJECT position, which failed
if the security plugin's all_shared_principals field (or any other ancillary
top-level field) happened to appear before the wrapper in the stored doc.

parseWrapper iterates every top-level field and dispatches to namedObject
the first time it hits a known wrapper (or the caller-supplied type), so
ancillary fields can appear on either side of the wrapper. Removes ~30 lines
of duplicated skip-loop logic.

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

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit cb17148

@cwperks
cwperks merged commit 867b9fd into opensearch-project:main Jul 24, 2026
11 checks passed
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