Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
cba6f83
Onboards to Resource Sharing and Authorization
DarshitChanpura Jun 20, 2026
b8fba5f
Adds tests for resource sharing feature
DarshitChanpura Jun 20, 2026
2576e6b
Fix resource types
DarshitChanpura Jun 20, 2026
eaa005a
Filter alerts by accessible monitor IDs when resource sharing is en…
DarshitChanpura Jul 20, 2026
cd35539
Merge remote-tracking branch 'upstream/main' into onbaord-resource-authz
DarshitChanpura Jul 20, 2026
ef94b4c
Adds a default access level
DarshitChanpura Jul 20, 2026
ed0997a
Removes duplicate sec plugin zip loading
DarshitChanpura Jul 20, 2026
4959318
Route access control through resource-sharing framework when enabled
DarshitChanpura Jul 20, 2026
1150f6d
Merge remote-tracking branch 'upstream/main' into onbaord-resource-authz
DarshitChanpura Jul 20, 2026
c405ac4
Avoid capturing ResourceSharingClient in lambdas to prevent NoClassDe…
DarshitChanpura Jul 20, 2026
fee8e21
Route shared-resource searches through PluginClient when
DarshitChanpura Jul 21, 2026
39e6af8
Expand resource-sharing IT coverage over transport-level interception
DarshitChanpura Jul 21, 2026
fccd755
Decouple ResourceSharingClientAccessor from security-spi type at clas…
DarshitChanpura Jul 21, 2026
132ab70
Gate resource-sharing paths on isFeatureEnabledForType
DarshitChanpura Jul 21, 2026
3423f88
Cover full access-level matrix in resource-sharing ITs
DarshitChanpura Jul 21, 2026
f181725
Expand resource-sharing IT coverage to a full behavior matrix
DarshitChanpura Jul 21, 2026
9c29f1b
Fix resource-sharing IT setup: batch role mapping, add index perms
DarshitChanpura Jul 21, 2026
cf5e603
Require explicit resourceType arg to shouldUseResourceAuthz
DarshitChanpura Jul 21, 2026
7cf03da
Configure protected_types for resource sharing in integTest cluster
DarshitChanpura Jul 22, 2026
8162e18
Bump common-utils dependency to track opensearch_build (3.8.0.0)
DarshitChanpura Jul 22, 2026
77a1998
Wire monitors and workflows into resource-sharing framework
DarshitChanpura Jul 22, 2026
f8d0e6d
Fix RSC IT suite: local helpers, revoke via PATCH, downgrade sequencing
DarshitChanpura Jul 22, 2026
4a43f4b
Remove RSC_MIGRATION.md scratch design doc
DarshitChanpura Jul 22, 2026
a550f12
Add POST /_plugins/_alerting/_migrate_to_rsc for RSC upgrade path
DarshitChanpura Jul 22, 2026
6560893
Purge legacy metadata docs during migrate + add E2E lifecycle IT
DarshitChanpura Jul 22, 2026
161502a
JobSweeper: skip ancillary top-level fields when detecting job type
DarshitChanpura Jul 22, 2026
1eab1cd
Drop top-level resource_type; classify via nested type paths
DarshitChanpura Jul 23, 2026
04e7d5f
Drop _migrate_to_rsc endpoint; rely on security plugin's classifier
DarshitChanpura Jul 23, 2026
99c95b9
Post-review cleanups: extension tests cover workflow provider; drop s…
DarshitChanpura Jul 23, 2026
1f7076a
Accept 403|404 on post-delete GET; ignore flaky alerts-inherit test
DarshitChanpura Jul 24, 2026
a01d541
Fix schema-version test expectation and restore stash pattern for com…
DarshitChanpura Jul 24, 2026
c507456
Unwrap CompletionException in SdkClient await so REST status survives
DarshitChanpura Jul 27, 2026
e7e43f5
Run RSC accessible-resource lookups under caller context; harden comm…
DarshitChanpura Jul 28, 2026
d8f115e
Initialize alerts index mapping in comment ITs so RSC monitor_id filt…
DarshitChanpura Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/security-test-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:
strategy:
matrix:
java: [ 21 ]
# empty string = resource sharing disabled, flag = enabled
resource_sharing_flag: ["", "-Dresource_sharing.enabled=true"]
needs: Get-CI-Image-Tag
# This job runs on Linux
runs-on: ubuntu-latest
Expand Down Expand Up @@ -46,12 +48,12 @@ jobs:
- name: Run integration tests
run: |
chown -R 1000:1000 `pwd`
su `id -un 1000` -c "./gradlew integTest -Dsecurity=true -Dhttps=true --tests '*IT'"
su `id -un 1000` -c "./gradlew integTest -Dsecurity=true -Dhttps=true ${{ matrix.resource_sharing_flag }} --tests '*IT'"

- name: Upload failed logs
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: failure()
with:
name: logs
name: logs-${{ matrix.resource_sharing_flag != '' && 'resource-sharing' || 'no-resource-sharing' }}
overwrite: 'true'
path: build/testclusters/integTest-*/logs/*
path: build/testclusters/integTest-*/logs/*
17 changes: 12 additions & 5 deletions alerting/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ opensearchplugin {
name 'opensearch-alerting'
description 'Amazon OpenSearch alerting plugin'
classname 'org.opensearch.alerting.AlertingPlugin'
extendedPlugins = ['lang-painless']
extendedPlugins = ['lang-painless', 'opensearch-security;optional=true']
}

publishing {
Expand Down Expand Up @@ -164,17 +164,19 @@ dependencies {
zipArchive group: 'org.opensearch.plugin', name:'opensearch-job-scheduler', version: "${opensearch_build}"
zipArchive group: 'org.opensearch.plugin', name:'opensearch-sql-plugin', version: "${opensearch_build}"

// Needed for security tests
if (securityEnabled) {
opensearchPlugin "org.opensearch.plugin:opensearch-security:${opensearch_build}@zip"
}
// Needed for security tests and to provide ResourceSharingClient class at runtime
// for the resource-sharing framework (matches pattern used in reporting plugin)
opensearchPlugin "org.opensearch.plugin:opensearch-security:${opensearch_build}@zip"

// Needed for BWC tests
opensearchPlugin "org.opensearch.plugin:alerting:${bwcPluginVersion}@zip"

compileOnly "org.opensearch.plugin:opensearch-scripting-painless-spi:${versions.opensearch}"
api "org.opensearch.plugin:percolator-client:${opensearch_version}"

// Resource sharing
compileOnly group: 'org.opensearch', name:'opensearch-security-spi', version:"${opensearch_build}"

// OpenSearch Nanny state
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:${kotlin_version}"
Expand Down Expand Up @@ -360,6 +362,10 @@ testClusters.integTest.nodes.each { node ->
node.setting("plugins.security.restapi.roles_enabled", "[\"all_access\", \"security_rest_api_access\"]")
node.setting("plugins.security.system_indices.enabled", "true")
node.setting("plugins.security.user_attribute_serialization.enabled", "true")
if (System.getProperty("resource_sharing.enabled") == "true") {
node.setting "plugins.security.experimental.resource_sharing.enabled", "true"
node.setting "plugins.security.experimental.resource_sharing.protected_types", "[\"monitor\", \"workflow\"]"
}
}
}

Expand All @@ -372,6 +378,7 @@ integTest {
systemProperty "security", System.getProperty("security")
systemProperty "user", System.getProperty("user", "admin")
systemProperty "password", System.getProperty("password", "admin")
systemProperty "resource_sharing.enabled", System.getProperty("resource_sharing.enabled")

// The 'doFirst' delays till execution time.
doFirst {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import org.opensearch.alerting.util.IndexUtils
import org.opensearch.alerting.util.MAX_SEARCH_SIZE
import org.opensearch.alerting.util.await
import org.opensearch.alerting.util.getBucketKeysHash
import org.opensearch.alerting.util.putDataObjectStashed
import org.opensearch.common.unit.TimeValue
import org.opensearch.common.xcontent.LoggingDeprecationHandler
import org.opensearch.common.xcontent.XContentHelper
Expand Down Expand Up @@ -557,7 +558,7 @@ class AlertService(
.overwriteIfExists(true)
.dataObject(ToXContentObject { builder, _ -> alert.toXContentWithUser(builder) })
.build()
val putResponse = sdkClient.putDataObjectAsync(putRequest).await()
val putResponse = sdkClient.putDataObjectStashed(putRequest, client.threadPool().threadContext)
if (putResponse.isFailed) {
throw ExceptionsHelper.convertToOpenSearchException(
putResponse.cause() ?: RuntimeException("Failed to upsert monitor error alert")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import org.opensearch.alerting.transport.TransportSearchEmailGroupAction
import org.opensearch.alerting.transport.TransportSearchMonitorAction
import org.opensearch.alerting.util.DocLevelMonitorQueries
import org.opensearch.alerting.util.MustacheTemplateService
import org.opensearch.alerting.util.PluginClient
import org.opensearch.alerting.util.destinationmigration.DestinationMigrationCoordinator
import org.opensearch.cluster.metadata.IndexNameExpressionResolver
import org.opensearch.cluster.node.DiscoveryNodes
Expand Down Expand Up @@ -128,6 +129,7 @@ import org.opensearch.core.xcontent.NamedXContentRegistry
import org.opensearch.core.xcontent.XContentParser
import org.opensearch.env.Environment
import org.opensearch.env.NodeEnvironment
import org.opensearch.identity.PluginSubject
import org.opensearch.index.IndexModule
import org.opensearch.indices.SystemIndexDescriptor
import org.opensearch.monitor.jvm.JvmStats
Expand All @@ -137,6 +139,7 @@ import org.opensearch.painless.spi.PainlessExtension
import org.opensearch.percolator.PercolatorPluginExt
import org.opensearch.plugins.ActionPlugin
import org.opensearch.plugins.ExtensiblePlugin
import org.opensearch.plugins.IdentityAwarePlugin
import org.opensearch.plugins.ReloadablePlugin
import org.opensearch.plugins.ScriptPlugin
import org.opensearch.plugins.SearchPlugin
Expand Down Expand Up @@ -165,7 +168,9 @@ import java.util.function.Supplier
* [BucketLevelTrigger.XCONTENT_REGISTRY], [ClusterMetricsInput.XCONTENT_REGISTRY] to the [NamedXContentRegistry] so that we are able to deserialize the custom named objects.
*/
internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, ReloadablePlugin,
SearchPlugin, SystemIndexPlugin, PercolatorPluginExt() {
SearchPlugin, SystemIndexPlugin, IdentityAwarePlugin, PercolatorPluginExt() {

private var pluginClient: PluginClient? = null

override fun getContextAllowlists(): Map<ScriptContext<*>, List<Allowlist>> {
val whitelist = AllowlistLoader.loadFromResourceFiles(javaClass, "org.opensearch.alerting.txt")
Expand Down Expand Up @@ -270,7 +275,7 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R
ActionPlugin.ActionHandler(AlertingActions.DELETE_COMMENT_ACTION_TYPE, TransportDeleteAlertingCommentAction::class.java),
ActionPlugin.ActionHandler(ExecuteWorkflowAction.INSTANCE, TransportExecuteWorkflowAction::class.java),
ActionPlugin.ActionHandler(GetRemoteIndexesAction.INSTANCE, TransportGetRemoteIndexesAction::class.java),
ActionPlugin.ActionHandler(DocLevelMonitorFanOutAction.INSTANCE, TransportDocLevelMonitorFanOutAction::class.java)
ActionPlugin.ActionHandler(DocLevelMonitorFanOutAction.INSTANCE, TransportDocLevelMonitorFanOutAction::class.java),
)
}

Expand Down Expand Up @@ -409,6 +414,9 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R
}
}

val pluginClientInstance = PluginClient(client)
this.pluginClient = pluginClientInstance

return listOf(
sweeper,
scheduler,
Expand All @@ -421,10 +429,15 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R
alertService,
triggerService,
sdkClient,
monitorJobPoller
monitorJobPoller,
pluginClientInstance
)
}

override fun assignSubject(pluginSubject: PluginSubject) {
pluginClient?.setSubject(pluginSubject)
}

override fun getSettings(): List<Setting<*>> {
return listOf(
ScheduledJobSettings.REQUEST_TIMEOUT,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.alerting

import org.opensearch.commons.alerting.model.ScheduledJob.Companion.SCHEDULED_JOBS_INDEX
import org.opensearch.security.spi.resources.ResourceProvider
import org.opensearch.security.spi.resources.ResourceSharingExtension
import org.opensearch.security.spi.resources.client.ResourceSharingClient

class AlertingResourceSharingExtension : ResourceSharingExtension {
/**
* Monitors and workflows share [SCHEDULED_JOBS_INDEX]. Each provider declares its own
* type-specific field paths so the security plugin can distinguish them without a
* top-level discriminator on the stored doc:
*
* - [ResourceProvider.typeField] — used by the postIndex hook to classify new writes.
* `monitor.type` is present on monitor docs and absent on workflow docs (and vice versa),
* so the framework iterates matching providers and picks the one whose typeField
* resolves non-null.
* - [ResourceProvider.ownerNamePath] / [ResourceProvider.ownerBackendRolesPath] — used by
* the security plugin's `POST /_plugins/_security/api/resources/migrate` endpoint when
* seeding sharing entries for legacy docs, so a single admin call can attribute owners
* across both types without pre-processing.
*/
override fun getResourceProviders(): Set<ResourceProvider> {
return setOf(
object : ResourceProvider {
override fun resourceType(): String = ResourceSharingUtils.MONITOR_RESOURCE_TYPE
override fun resourceIndexName(): String = SCHEDULED_JOBS_INDEX

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Alerts and comments are treated as subordinate resources rather than registered resource types, so we don't register their indices with the sharing framework. Instead, access is inherited from the underlying monitor:

  • TransportGetAlertsAction / TransportGetWorkflowAlertsAction call rsc.getAccessibleResourceIds("monitor", ...) and add a monitor_id terms filter to the alert search.
  • TransportSearchAlertingCommentAction does the same via alert IDs derived from accessible monitors.
  • TransportIndexAlertingCommentAction / TransportDeleteAlertingCommentAction rely on the ActionFilter blocking the underlying alert fetch (via DocRequest on the requests, which points at the monitor).

New tests bob cannot see alice's monitor alerts without share and bob can see alice's monitor alerts after share in SecureResourceSharingMonitorRestApiIT cover this.

override fun typeField(): String = "monitor.type"
override fun ownerNamePath(): String = "/monitor/user/name"
override fun ownerBackendRolesPath(): String = "/monitor/user/backend_roles"
},
object : ResourceProvider {
override fun resourceType(): String = ResourceSharingUtils.WORKFLOW_RESOURCE_TYPE
override fun resourceIndexName(): String = SCHEDULED_JOBS_INDEX
override fun typeField(): String = "workflow.type"
override fun ownerNamePath(): String = "/workflow/user/name"
override fun ownerBackendRolesPath(): String = "/workflow/user/backend_roles"
}
)
}

override fun assignResourceSharingClient(client: ResourceSharingClient?) {
ResourceSharingClientAccessor.setResourceSharingClient(client)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import org.opensearch.action.admin.indices.stats.IndicesStatsResponse
import org.opensearch.alerting.opensearchapi.suspendUntil
import org.opensearch.alerting.settings.AlertingSettings
import org.opensearch.alerting.util.IndexUtils
import org.opensearch.alerting.util.await
import org.opensearch.alerting.util.getDataObjectStashed
import org.opensearch.alerting.util.putDataObjectStashed
import org.opensearch.cluster.service.ClusterService
import org.opensearch.common.settings.Settings
import org.opensearch.common.unit.TimeValue
Expand Down Expand Up @@ -98,7 +99,10 @@ object MonitorMetadataService :
putRequestBuilder.overwriteIfExists(false)
}

val putResponse = sdkClient.putDataObjectAsync(putRequestBuilder.build()).await()
val putResponse = sdkClient.putDataObjectStashed(
putRequestBuilder.build(),
client.threadPool().threadContext,
)
if (putResponse.isFailed) {
val failureReason = "The upsert metadata call failed: ${putResponse.cause()?.message}"
log.error(failureReason)
Expand Down Expand Up @@ -178,7 +182,7 @@ object MonitorMetadataService :
.tenantId(currentTenantId())
.build()

val response = sdkClient.getDataObjectAsync(getRequest).await()
val response = sdkClient.getDataObjectStashed(getRequest, client.threadPool().threadContext)
val getResponse = response.getResponse()
return if (getResponse != null && getResponse.isExists) {
val xcp = XContentHelper.createParser(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
package org.opensearch.alerting

import org.opensearch.security.spi.resources.client.ResourceSharingClient

/**
* Accessor for resource sharing client.
*
* The internal field is typed as [Any?] so that loading this class does NOT trigger resolution of
* [ResourceSharingClient] — which lives in the security-spi jar and is absent at runtime when the
* security plugin is not installed. Callers that need methods on the client cast the result.
*/
object ResourceSharingClientAccessor {

@Volatile
private var client: Any? = null

/**
* Set the resource sharing client. Only called by [AlertingResourceSharingExtension.assignResourceSharingClient]
* which is invoked by the security plugin — so [ResourceSharingClient] is guaranteed to be on the classpath
* at that point.
*/
@JvmStatic
fun setResourceSharingClient(client: ResourceSharingClient?) {
this.client = client
}

/**
* Get the resource sharing client, or null if the security plugin is not loaded / resource sharing is disabled.
* Returns [Any?] to avoid linking [ResourceSharingClient] in callers when the security plugin is absent.
* Callers that need to invoke methods should cast: `getResourceSharingClient() as ResourceSharingClient`.
*/
@JvmStatic
fun getResourceSharingClient(): Any? = client

/**
* Clear the client (useful in tests).
*/
@JvmStatic
fun clear() {
client = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
package org.opensearch.alerting

import org.opensearch.security.spi.resources.client.ResourceSharingClient

/**
* Shared helpers for the resource-sharing framework.
*
* The [ResourceSharingClient] class is referenced only inside method bodies (never as a field type)
* so this object can be class-loaded when the security plugin is absent without triggering
* [NoClassDefFoundError]. Callers should invoke [shouldUseResourceAuthz] rather than reading the
* accessor directly.
*/
internal object ResourceSharingUtils {

/** Resource type registered by [AlertingResourceSharingExtension] for monitors. */
const val MONITOR_RESOURCE_TYPE = "monitor"

/** Resource type registered by [AlertingResourceSharingExtension] for workflows. */
const val WORKFLOW_RESOURCE_TYPE = "workflow"

/**
* Returns true only when the security plugin is loaded AND the resource-sharing feature is enabled
* for [resourceType]. A non-null accessor client alone is insufficient — the plugin may be present
* with the RSC feature flag disabled.
*/
fun shouldUseResourceAuthz(resourceType: String): Boolean {
val client = ResourceSharingClientAccessor.getResourceSharingClient() ?: return false
return (client as ResourceSharingClient).isFeatureEnabledForType(resourceType)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import org.opensearch.alerting.opensearchapi.suspendUntil
import org.opensearch.alerting.util.ScheduledJobUtils.Companion.WORKFLOW_DELEGATE_PATH
import org.opensearch.alerting.util.ScheduledJobUtils.Companion.WORKFLOW_MONITOR_PATH
import org.opensearch.alerting.util.await
import org.opensearch.alerting.util.deleteDataObjectStashed
import org.opensearch.alerting.util.use
import org.opensearch.commons.alerting.action.DeleteMonitorResponse
import org.opensearch.commons.alerting.model.Monitor
Expand Down Expand Up @@ -88,7 +89,7 @@ object DeleteMonitorService :
.tenantId(tenantId)
.refreshPolicy(refreshPolicy)
.build()
val deleteResponse = sdkClient.deleteDataObjectAsync(deleteRequest).await()
val deleteResponse = sdkClient.deleteDataObjectStashed(deleteRequest, client.threadPool().threadContext)
return DeleteMonitorResponse(deleteResponse.id(), deleteResponse.deleteResponse().version)
}

Expand All @@ -101,7 +102,7 @@ object DeleteMonitorService :
.refreshPolicy(RefreshPolicy.IMMEDIATE)
.build()
try {
val deleteResponse = sdkClient.deleteDataObjectAsync(deleteRequest).await()
val deleteResponse = sdkClient.deleteDataObjectStashed(deleteRequest, client.threadPool().threadContext)
log.debug("Monitor metadata: ${deleteResponse.id()} deletion result: ${deleteResponse.status()}")
} catch (e: Exception) {
// we only log the error and don't fail the request because if monitor document has been deleted,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import org.opensearch.action.search.SearchResponse
import org.opensearch.action.support.ActionFilters
import org.opensearch.action.support.HandledTransportAction
import org.opensearch.alerting.AlertingPlugin
import org.opensearch.alerting.ResourceSharingUtils
import org.opensearch.alerting.settings.AlertingSettings
import org.opensearch.alerting.util.await
import org.opensearch.alerting.util.use
Expand Down Expand Up @@ -94,7 +95,8 @@ class TransportAcknowledgeAlertAction @Inject constructor(
?: recreateObject(acknowledgeAlertRequest) { AcknowledgeAlertRequest(it) }
val user = readUserFromThreadContext(client)

if (!validateUserBackendRoles(user, actionListener)) {
val useRsc = ResourceSharingUtils.shouldUseResourceAuthz(ResourceSharingUtils.MONITOR_RESOURCE_TYPE)
if (!useRsc && !validateUserBackendRoles(user, actionListener)) {
return
}

Expand All @@ -118,7 +120,10 @@ class TransportAcknowledgeAlertAction @Inject constructor(
return@launch
}

val canAccess = user == null || !doFilterForUser(user) ||
// when resource sharing is enabled, security plugin gates access at the index layer
val canAccess = useRsc ||
user == null ||
!doFilterForUser(user) ||
checkUserPermissionsWithResource(user, monitor.user, actionListener, "monitor", request.monitorId)

if (canAccess) {
Expand Down
Loading
Loading