diff --git a/.github/workflows/security-notifications-test-workflow.yml b/.github/workflows/security-notifications-test-workflow.yml index ed51a8c1071..94fc47b1b58 100644 --- a/.github/workflows/security-notifications-test-workflow.yml +++ b/.github/workflows/security-notifications-test-workflow.yml @@ -17,6 +17,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 @@ -46,12 +48,12 @@ jobs: run: | cd notifications 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/* diff --git a/notifications/notifications/build.gradle b/notifications/notifications/build.gradle index 277b055dd8a..0d855ac8a15 100644 --- a/notifications/notifications/build.gradle +++ b/notifications/notifications/build.gradle @@ -46,7 +46,7 @@ opensearchplugin { name 'opensearch-notifications' description 'OpenSearch Notifications Plugin' classname 'org.opensearch.notifications.NotificationPlugin' - extendedPlugins = ['opensearch-notifications-core'] + extendedPlugins = ['opensearch-notifications-core', 'opensearch-security;optional=true'] } publishing { @@ -129,6 +129,9 @@ configurations.all { : versions.jackson details.useVersion version } + if (details.requested.group.startsWith('tools.jackson')) { + details.useVersion versions.jackson3 + } } force "commons-logging:commons-logging:1.3.5" force "commons-codec:commons-codec:1.17.1" @@ -182,7 +185,8 @@ dependencies { // implementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" // implementation "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson_annotations}" - // Needed for security tests + // Resource sharing + compileOnly group: 'org.opensearch', name:'opensearch-security-spi', version:"${opensearch_build}" if (securityEnabled) { opensearchPlugin "org.opensearch.plugin:opensearch-security:${opensearch_build}@zip" } @@ -282,6 +286,10 @@ afterEvaluate { node.setting("plugins.security.check_snapshot_restore_write_privileges", "true") node.setting("plugins.security.restapi.roles_enabled", "[\"all_access\", \"security_rest_api_access\"]") node.setting("plugins.security.system_indices.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", "[\"notification_config\"]" + } } } } @@ -322,6 +330,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") // Tell the test JVM if the cluster JVM is running under a debugger so that tests can use longer timeouts for // requests. The 'doFirst' delays reading the debug setting on the cluster till execution time. doFirst { @@ -349,6 +358,16 @@ integTest { } } + if (System.getProperty("resource_sharing.enabled") != "true") { + filter { + excludeTestsMatching "org.opensearch.integtest.ResourceSharingNotificationIT" + } + } else { + filter { + includeTestsMatching "org.opensearch.integtest.ResourceSharingNotificationIT" + } + } + if (System.getProperty("tests.rest.bwcsuite") == null) { filter { excludeTestsMatching "org.opensearch.integtest.bwc.*IT" diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationPlugin.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationPlugin.kt index 1530305bfea..1c1925bcf4f 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationPlugin.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationPlugin.kt @@ -21,6 +21,7 @@ import org.opensearch.core.common.io.stream.NamedWriteableRegistry import org.opensearch.core.xcontent.NamedXContentRegistry import org.opensearch.env.Environment import org.opensearch.env.NodeEnvironment +import org.opensearch.identity.PluginSubject import org.opensearch.indices.SystemIndexDescriptor import org.opensearch.notifications.action.CreateNotificationConfigAction import org.opensearch.notifications.action.DeleteNotificationConfigAction @@ -47,8 +48,10 @@ import org.opensearch.notifications.settings.PluginSettings.REMOTE_METADATA_SERV import org.opensearch.notifications.settings.PluginSettings.REMOTE_METADATA_STORE_TYPE import org.opensearch.notifications.spi.NotificationCore import org.opensearch.notifications.spi.NotificationCoreExtension +import org.opensearch.notifications.util.PluginClient import org.opensearch.notifications.util.SecureIndexClient import org.opensearch.plugins.ActionPlugin +import org.opensearch.plugins.IdentityAwarePlugin import org.opensearch.plugins.Plugin import org.opensearch.plugins.SystemIndexPlugin import org.opensearch.remote.metadata.client.impl.SdkClientFactory @@ -72,9 +75,10 @@ import java.util.function.Supplier * Entry point of the OpenSearch Notifications plugin * This class initializes the rest handlers. */ -class NotificationPlugin : ActionPlugin, Plugin(), NotificationCoreExtension, SystemIndexPlugin { +class NotificationPlugin : ActionPlugin, Plugin(), NotificationCoreExtension, SystemIndexPlugin, IdentityAwarePlugin { lateinit var clusterService: ClusterService // initialized in createComponents() + private var pluginClient: PluginClient? = null internal companion object { private val log by logger(NotificationPlugin::class.java) @@ -146,10 +150,29 @@ class NotificationPlugin : ActionPlugin, Plugin(), NotificationCoreExtension, Sy client.threadPool().executor(ThreadPool.Names.GENERIC) ) PluginSettings.addSettingsUpdateConsumer(clusterService) - NotificationConfigIndex.initialize(sdkClient, client, clusterService) + val pluginClientInstance = PluginClient(client) + this.pluginClient = pluginClientInstance + val searchSdkClient = SdkClientFactory.createSdkClient( + pluginClientInstance, + xContentRegistry, + mapOf( + REMOTE_METADATA_TYPE_KEY to REMOTE_METADATA_STORE_TYPE.get(settings), + REMOTE_METADATA_ENDPOINT_KEY to REMOTE_METADATA_ENDPOINT.get(settings), + REMOTE_METADATA_REGION_KEY to REMOTE_METADATA_REGION.get(settings), + REMOTE_METADATA_SERVICE_NAME_KEY to REMOTE_METADATA_SERVICE_NAME.get(settings), + TENANT_AWARE_KEY to MULTI_TENANCY_ENABLED.get(settings).toString(), + TENANT_ID_FIELD_KEY to "tenant_id" + ), + client.threadPool().executor(ThreadPool.Names.GENERIC) + ) + NotificationConfigIndex.initialize(sdkClient, searchSdkClient, client, clusterService) ConfigIndexingActions.initialize(NotificationConfigIndex, UserAccessManager) SendMessageActionHelper.initialize(NotificationConfigIndex, UserAccessManager) - return listOf(sdkClient) + return listOf(sdkClient, pluginClientInstance) + } + + override fun assignSubject(pluginSubject: PluginSubject) { + pluginClient?.setSubject(pluginSubject) } /** diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationsResourceSharingExtension.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationsResourceSharingExtension.kt new file mode 100644 index 00000000000..23bf40c3640 --- /dev/null +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationsResourceSharingExtension.kt @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.opensearch.notifications + +import org.opensearch.notifications.index.NotificationConfigIndex +import org.opensearch.security.spi.resources.ResourceProvider +import org.opensearch.security.spi.resources.ResourceSharingExtension +import org.opensearch.security.spi.resources.client.ResourceSharingClient + +class NotificationsResourceSharingExtension : ResourceSharingExtension { + + companion object { + const val RESOURCE_TYPE = "notification_config" + } + + override fun getResourceProviders(): Set { + return setOf( + object : ResourceProvider { + override fun resourceType(): String = RESOURCE_TYPE + override fun resourceIndexName(): String = NotificationConfigIndex.INDEX_NAME + } + ) + } + + override fun assignResourceSharingClient(client: ResourceSharingClient?) { + ResourceSharingClientAccessor.setResourceSharingClient(client) + } +} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/ResourceSharingClientAccessor.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/ResourceSharingClientAccessor.kt new file mode 100644 index 00000000000..3323d0d2d25 --- /dev/null +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/ResourceSharingClientAccessor.kt @@ -0,0 +1,29 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.opensearch.notifications + +import org.opensearch.security.spi.resources.client.ResourceSharingClient + +/** + * Accessor for resource sharing client + */ +object ResourceSharingClientAccessor { + + @Volatile + private var client: ResourceSharingClient? = null + + @JvmStatic + fun setResourceSharingClient(client: ResourceSharingClient?) { + this.client = client + } + + @JvmStatic + fun getResourceSharingClient(): ResourceSharingClient? = client + + @JvmStatic + fun clear() { + client = null + } +} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt index ec6ba961767..f7178445cfe 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt @@ -38,6 +38,7 @@ import org.opensearch.notifications.metrics.Metrics import org.opensearch.notifications.model.DocMetadata import org.opensearch.notifications.model.NotificationConfigDoc import org.opensearch.notifications.security.UserAccess +import org.opensearch.notifications.security.UserAccessManager import java.time.Instant /** * NotificationConfig indexing operation actions. @@ -310,6 +311,9 @@ object ConfigIndexingActions { */ private suspend fun info(configIds: Set, user: User?): GetNotificationConfigResponse { log.info("$LOG_PREFIX:NotificationConfig-info $configIds") + configIds.forEach { id -> + UserAccessManager.verifyResourceAccess(id, "cluster:admin/opensearch/notifications/configs/get") + } val configDocs = operations.getNotificationConfigs(configIds) if (configDocs.size != configIds.size) { val mutableSet = configIds.toMutableSet() @@ -441,6 +445,9 @@ object ConfigIndexingActions { private suspend fun delete(configIds: Set, user: User?): DeleteNotificationConfigResponse { log.info("$LOG_PREFIX:NotificationConfig-delete $configIds") userAccess.validateUser(user) + configIds.forEach { id -> + UserAccessManager.verifyResourceAccess(id, "cluster:admin/opensearch/notifications/configs/delete") + } val configDocs = operations.getNotificationConfigs(configIds) if (configDocs.size != configIds.size) { val mutableSet = configIds.toMutableSet() diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/NotificationConfigIndex.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/NotificationConfigIndex.kt index 8a4db936e61..6cb80c61061 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/NotificationConfigIndex.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/NotificationConfigIndex.kt @@ -83,6 +83,7 @@ internal object NotificationConfigIndex : ConfigOperations { private lateinit var client: Client private lateinit var clusterService: ClusterService private lateinit var sdkClient: SdkClient + private lateinit var searchSdkClient: SdkClient private val searchHitParser = object : SearchResults.SearchHitParser { override fun parse(searchHit: SearchHit): NotificationConfigInfo { @@ -105,10 +106,11 @@ internal object NotificationConfigIndex : ConfigOperations { /** * {@inheritDoc} */ - fun initialize(sdkClient: SdkClient, client: Client, clusterService: ClusterService) { + fun initialize(sdkClient: SdkClient, searchSdkClient: SdkClient, client: Client, clusterService: ClusterService) { NotificationConfigIndex.client = SecureIndexClient(client) NotificationConfigIndex.clusterService = clusterService NotificationConfigIndex.sdkClient = sdkClient + NotificationConfigIndex.searchSdkClient = searchSdkClient } private fun getSchemaVersionFromIndexMapping(indexMapping: Map?): Int { @@ -290,8 +292,8 @@ internal object NotificationConfigIndex : ConfigOperations { .searchSourceBuilder(sourceBuilder) .build() - val response: SearchResponse = sdkClient.suspendUntilTimeout(PluginSettings.operationTimeoutMs) { - sdkClient.searchDataObjectAsync(searchRequest).whenComplete(it) + val response: SearchResponse = searchSdkClient.suspendUntilTimeout(PluginSettings.operationTimeoutMs) { + searchSdkClient.searchDataObjectAsync(searchRequest).whenComplete(it) } val result = NotificationConfigSearchResult(request.fromIndex.toLong(), response, searchHitParser) log.info( diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/security/UserAccessManager.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/security/UserAccessManager.kt index b4fa20c71d5..9c39449e030 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/security/UserAccessManager.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/security/UserAccessManager.kt @@ -5,11 +5,17 @@ package org.opensearch.notifications.security +import kotlinx.coroutines.suspendCancellableCoroutine import org.opensearch.OpenSearchStatusException import org.opensearch.commons.authuser.User +import org.opensearch.core.action.ActionListener import org.opensearch.core.rest.RestStatus +import org.opensearch.notifications.NotificationsResourceSharingExtension.Companion.RESOURCE_TYPE +import org.opensearch.notifications.ResourceSharingClientAccessor import org.opensearch.notifications.settings.FilterByBackendRolesAccessStrategy import org.opensearch.notifications.settings.PluginSettings +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException /** * Class for checking/filtering user access. @@ -17,10 +23,16 @@ import org.opensearch.notifications.settings.PluginSettings internal object UserAccessManager : UserAccess { const val ADMIN_ROLE = "all_access" + private fun isResourceSharingEnabled(): Boolean { + val client = ResourceSharingClientAccessor.getResourceSharingClient() + return client != null && client.isFeatureEnabledForType(RESOURCE_TYPE) + } + /** * {@inheritDoc} */ override fun validateUser(user: User?) { + if (isResourceSharingEnabled()) return if (PluginSettings.isRbacEnabled() && user?.backendRoles.isNullOrEmpty()) { throw OpenSearchStatusException( "User doesn't have backend roles configured. Contact administrator.", @@ -33,7 +45,7 @@ internal object UserAccessManager : UserAccess { * {@inheritDoc} */ override fun getAllAccessInfo(user: User?): List { - if (user == null) { // Filtering is disabled + if (isResourceSharingEnabled() || user == null) { return listOf() } return user.backendRoles @@ -43,7 +55,7 @@ internal object UserAccessManager : UserAccess { * {@inheritDoc} */ override fun getSearchAccessInfo(user: User?): List { - if (user == null || !PluginSettings.isRbacEnabled() || user.roles.contains(ADMIN_ROLE)) { // Filtering is disabled + if (isResourceSharingEnabled() || user == null || !PluginSettings.isRbacEnabled() || user.roles.contains(ADMIN_ROLE)) { return listOf() } return user.backendRoles @@ -67,11 +79,36 @@ internal object UserAccessManager : UserAccess { * {@inheritDoc} */ override fun doesUserHaveAccess(user: User?, access: List): Boolean { - if (user == null || !PluginSettings.isRbacEnabled()) { // Filtering is disabled + if (isResourceSharingEnabled() || user == null || !PluginSettings.isRbacEnabled()) { return true } - // User has access to resource if resource is public i.e. no access roles attached, user is an admin user or there is any intersection - // between user backend roles and access roles return access.isEmpty() || user.roles.contains(ADMIN_ROLE) || checkUserBackendRolesAccess(user.backendRoles, access) } + + /** + * Verify resource access via the security plugin's ResourceSharingClient. + * Used for multi-ID requests where DocRequest.id() returns null and the + * transport-level ResourceAccessEvaluator is skipped. + */ + suspend fun verifyResourceAccess(resourceId: String, action: String) { + val client = ResourceSharingClientAccessor.getResourceSharingClient() ?: return + if (!client.isFeatureEnabledForType(RESOURCE_TYPE)) return + val hasAccess = suspendCancellableCoroutine { cont -> + client.verifyAccess( + resourceId, + RESOURCE_TYPE, + action, + object : ActionListener { + override fun onResponse(response: Boolean) = cont.resume(response) + override fun onFailure(e: Exception) = cont.resumeWithException(e) + } + ) + } + if (!hasAccess) { + throw OpenSearchStatusException( + "no permissions for [$action] on resource [$resourceId]", + RestStatus.FORBIDDEN + ) + } + } } diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/util/PluginClient.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/util/PluginClient.kt new file mode 100644 index 00000000000..7f5034f3e1f --- /dev/null +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/util/PluginClient.kt @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.opensearch.notifications.util + +import org.opensearch.action.ActionRequest +import org.opensearch.action.ActionType +import org.opensearch.commons.utils.logger +import org.opensearch.core.action.ActionListener +import org.opensearch.core.action.ActionResponse +import org.opensearch.identity.Subject +import org.opensearch.transport.client.Client +import org.opensearch.transport.client.FilterClient + +class PluginClient(delegate: Client) : FilterClient(delegate) { + + private var subject: Subject? = null + + companion object { + private val log by logger(PluginClient::class.java) + } + + fun setSubject(subject: Subject) { + this.subject = subject + } + + @Suppress("UNCHECKED_CAST") + override fun doExecute( + action: ActionType, + request: Request, + listener: ActionListener + ) { + val currentSubject = subject + ?: error("PluginClient is not initialized.") + + val storedContext = threadPool().threadContext.newStoredContext(false) + + try { + currentSubject.runAs { + super.doExecute( + action, + request, + object : ActionListener { + override fun onResponse(response: Response) { + storedContext.restore() + listener.onResponse(response) + } + + override fun onFailure(e: Exception) { + storedContext.restore() + listener.onFailure(e) + } + } + ) + } + } catch (e: Exception) { + storedContext.restore() + listener.onFailure(e) + } + } +} diff --git a/notifications/notifications/src/main/resources/META-INF/services/org.opensearch.security.spi.resources.ResourceSharingExtension b/notifications/notifications/src/main/resources/META-INF/services/org.opensearch.security.spi.resources.ResourceSharingExtension new file mode 100644 index 00000000000..04d560d7e66 --- /dev/null +++ b/notifications/notifications/src/main/resources/META-INF/services/org.opensearch.security.spi.resources.ResourceSharingExtension @@ -0,0 +1,4 @@ +# Copyright OpenSearch Contributors +# SPDX-License-Identifier: Apache-2.0 + +org.opensearch.notifications.NotificationsResourceSharingExtension diff --git a/notifications/notifications/src/main/resources/notifications-config-mapping.yml b/notifications/notifications/src/main/resources/notifications-config-mapping.yml index 073d0f987b6..f2c3de12eba 100644 --- a/notifications/notifications/src/main/resources/notifications-config-mapping.yml +++ b/notifications/notifications/src/main/resources/notifications-config-mapping.yml @@ -8,7 +8,7 @@ # "dynamic" is set to "false" so that only specified fields are indexed instead of all fields. dynamic: false _meta: - schema_version: 2 + schema_version: 3 properties: metadata: type: object @@ -142,4 +142,6 @@ properties: type: text fields: keyword: - type: keyword \ No newline at end of file + type: keyword + all_shared_principals: + type: keyword \ No newline at end of file diff --git a/notifications/notifications/src/main/resources/resource-access-levels.yml b/notifications/notifications/src/main/resources/resource-access-levels.yml new file mode 100644 index 00000000000..f21b2a4da9e --- /dev/null +++ b/notifications/notifications/src/main/resources/resource-access-levels.yml @@ -0,0 +1,26 @@ +# For resource-access-management +resource_types: + notification_config: + notifications_read_only: + default: true + allowed_actions: + - 'cluster:admin/opensearch/notifications/configs/get' + - 'cluster:admin/opensearch/notifications/channels/get' + - 'cluster:admin/opensearch/notifications/features' + + notifications_read_write: + allowed_actions: + - 'cluster:admin/opensearch/notifications/configs/*' + - 'cluster:admin/opensearch/notifications/channels/get' + - 'cluster:admin/opensearch/notifications/features' + - 'cluster:admin/opensearch/notifications/feature/send' + - 'cluster:admin/opensearch/notifications/test_notification' + + notifications_full_access: + allowed_actions: + - 'cluster:admin/opensearch/notifications/configs/*' + - 'cluster:admin/opensearch/notifications/channels/get' + - 'cluster:admin/opensearch/notifications/features' + - 'cluster:admin/opensearch/notifications/feature/send' + - 'cluster:admin/opensearch/notifications/test_notification' + - 'cluster:admin/security/resource/share' diff --git a/notifications/notifications/src/test/java/org/opensearch/notifications/ResourceSharingTests.kt b/notifications/notifications/src/test/java/org/opensearch/notifications/ResourceSharingTests.kt new file mode 100644 index 00000000000..c137526c0ac --- /dev/null +++ b/notifications/notifications/src/test/java/org/opensearch/notifications/ResourceSharingTests.kt @@ -0,0 +1,66 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.notifications + +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.Mockito.mock +import org.opensearch.notifications.index.NotificationConfigIndex +import org.opensearch.security.spi.resources.client.ResourceSharingClient +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertSame + +class ResourceSharingTests { + + @BeforeEach + fun setup() { + ResourceSharingClientAccessor.clear() + } + + @Test + fun `get client returns null when not set`() { + assertNull(ResourceSharingClientAccessor.getResourceSharingClient()) + } + + @Test + fun `set and get client`() { + val mockClient = mock(ResourceSharingClient::class.java) + ResourceSharingClientAccessor.setResourceSharingClient(mockClient) + assertSame(mockClient, ResourceSharingClientAccessor.getResourceSharingClient()) + } + + @Test + fun `clear resets client to null`() { + val mockClient = mock(ResourceSharingClient::class.java) + ResourceSharingClientAccessor.setResourceSharingClient(mockClient) + ResourceSharingClientAccessor.clear() + assertNull(ResourceSharingClientAccessor.getResourceSharingClient()) + } + + @Test + fun `extension returns one provider`() { + val extension = NotificationsResourceSharingExtension() + val providers = extension.getResourceProviders() + assertEquals(1, providers.size) + } + + @Test + fun `provider has correct type and index`() { + val extension = NotificationsResourceSharingExtension() + val provider = extension.getResourceProviders().first() + assertEquals("notification_config", provider.resourceType()) + assertEquals(NotificationConfigIndex.INDEX_NAME, provider.resourceIndexName()) + } + + @Test + fun `assignResourceSharingClient sets client`() { + val extension = NotificationsResourceSharingExtension() + val mockClient = mock(ResourceSharingClient::class.java) + extension.assignResourceSharingClient(mockClient) + assertSame(mockClient, ResourceSharingClientAccessor.getResourceSharingClient()) + } +} diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/EmailGroupAccessIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/EmailGroupAccessIT.kt index 36e4f183adc..dc8acc4327e 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/EmailGroupAccessIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/EmailGroupAccessIT.kt @@ -28,8 +28,8 @@ class EmailGroupAccessIT : PluginRestTestCase() { @BeforeClass @JvmStatic fun setup() { - // things to execute once and keep around for the class org.junit.Assume.assumeTrue(System.getProperty("https", "false")!!.toBoolean()) + org.junit.Assume.assumeFalse(System.getProperty("resource_sharing.enabled", "false")!!.toBoolean()) } } diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/EmailNotificationAccessIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/EmailNotificationAccessIT.kt index db28d3dea78..9ea645c8349 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/EmailNotificationAccessIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/EmailNotificationAccessIT.kt @@ -25,8 +25,8 @@ class EmailNotificationAccessIT : PluginRestTestCase() { @BeforeClass @JvmStatic fun setup() { - // things to execute once and keep around for the class org.junit.Assume.assumeTrue(System.getProperty("https", "false")!!.toBoolean()) + org.junit.Assume.assumeFalse(System.getProperty("resource_sharing.enabled", "false")!!.toBoolean()) } } diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/PluginRestTestCase.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/PluginRestTestCase.kt index 8ab9e55dc79..7a0faa14220 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/PluginRestTestCase.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/PluginRestTestCase.kt @@ -74,7 +74,7 @@ abstract class PluginRestTestCase : OpenSearchRestTestCase() { if (preservePluginIndicesAfterTest()) return val pluginIndices = listOf(".opensearch-notifications-config") - val response = client().performRequest(Request("GET", "/_cat/indices?format=json&expand_wildcards=all")) + val response = adminClient().performRequest(Request("GET", "/_cat/indices?format=json&expand_wildcards=all")) val xContentType = MediaType.fromMediaType(response.entity.contentType) xContentType.xContent().createParser( NamedXContentRegistry.EMPTY, @@ -180,12 +180,12 @@ abstract class PluginRestTestCase : OpenSearchRestTestCase() { "\"attributes\": {\n" + "}} " request.setJsonEntity(entity) - client().performRequest(request) + adminClient().performRequest(request) } fun deleteUser(name: String) { val request = Request(RestRequest.Method.DELETE.name, "/_plugins/_security/api/internalusers/$name") - executeRequest(request, RestStatus.OK.status) + adminClient().performRequest(request) } fun createUserRolesMapping(role: String, users: Array) { @@ -197,7 +197,7 @@ abstract class PluginRestTestCase : OpenSearchRestTestCase() { " \"users\" : [$usersStr]\n" + "}" request.setJsonEntity(entity) - client().performRequest(request) + adminClient().performRequest(request) } fun addPatchUserRolesMapping(role: String, users: Array) { @@ -206,12 +206,12 @@ abstract class PluginRestTestCase : OpenSearchRestTestCase() { val entity = "[{\n" + " \"op\" : \"add\",\n" + - " \"path\" : \"users\",\n" + + " \"path\" : \"/users\",\n" + " \"value\" : [$usersStr]\n" + "}]" request.setJsonEntity(entity) - client().performRequest(request) + adminClient().performRequest(request) } fun removePatchUserRolesMapping(role: String, users: Array) { @@ -220,17 +220,17 @@ abstract class PluginRestTestCase : OpenSearchRestTestCase() { val entity = "[{\n" + " \"op\" : \"remove\",\n" + - " \"path\" : \"users\",\n" + + " \"path\" : \"/users\",\n" + " \"value\" : [$usersStr]\n" + "}]" request.setJsonEntity(entity) - client().performRequest(request) + adminClient().performRequest(request) } fun deleteUserRolesMapping(role: String) { val request = Request("DELETE", "/_plugins/_security/api/rolesmapping/$role") - client().performRequest(request) + adminClient().performRequest(request) } fun createCustomRole(name: String, clusterPermissions: String?) { @@ -243,12 +243,12 @@ abstract class PluginRestTestCase : OpenSearchRestTestCase() { } """.trimIndent() request.setJsonEntity(entity) - client().performRequest(request) + adminClient().performRequest(request) } fun deleteCustomRole(name: String) { val request = Request("DELETE", "/_plugins/_security/api/roles/$name") - client().performRequest(request) + adminClient().performRequest(request) } fun createUserWithRoles(user: String, password: String, role: String, backendRole: String) { @@ -516,8 +516,8 @@ abstract class PluginRestTestCase : OpenSearchRestTestCase() { @Throws(IOException::class) protected open fun wipeAllClusterSettings() { - updateClusterSettings(ClusterSetting("persistent", "*", null)) - updateClusterSettings(ClusterSetting("transient", "*", null)) + updateClusterSettings(ClusterSetting("persistent", "*", null), adminClient()) + updateClusterSettings(ClusterSetting("transient", "*", null), adminClient()) } protected fun getCurrentMappingsSchemaVersion(): Int { @@ -556,7 +556,7 @@ abstract class PluginRestTestCase : OpenSearchRestTestCase() { } } refreshRequest.setOptions(requestOptions) - client().performRequest(refreshRequest) + adminClient().performRequest(refreshRequest) } protected class ClusterSetting(val type: String, val name: String, var value: Any?) { diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/ResourceSharingNotificationIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/ResourceSharingNotificationIT.kt new file mode 100644 index 00000000000..4fe0f2836ed --- /dev/null +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/ResourceSharingNotificationIT.kt @@ -0,0 +1,482 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.integtest + +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.BeforeClass +import org.opensearch.client.Request +import org.opensearch.client.RestClient +import org.opensearch.commons.notifications.model.ConfigType +import org.opensearch.commons.rest.SecureRestClientBuilder +import org.opensearch.core.rest.RestStatus +import org.opensearch.notifications.NotificationPlugin +import org.opensearch.rest.RestRequest + +/** + * Integration tests for Resource Sharing feature with Notifications plugin. + * Only runs when both security and resource_sharing are enabled. + * + * All users have the same cluster-level role (notifications_full_access) which grants + * all notification actions + resource sharing. The tests verify that resource-level + * access control (via access levels in resource-access-levels.yml) correctly restricts + * what shared users can do regardless of their cluster permissions. + */ +class ResourceSharingNotificationIT : PluginRestTestCase() { + + companion object { + @BeforeClass + @JvmStatic + fun setup() { + org.junit.Assume.assumeTrue(System.getProperty("https", "false")!!.toBoolean()) + org.junit.Assume.assumeTrue(System.getProperty("resource_sharing.enabled", "false")!!.toBoolean()) + } + } + + private val notificationsFullAccessRole = "notifications_full_access" + private val aliceUser = "rs_alice" + private val alicePassword = "TopSecret_1234%Alice" + private val bobUser = "rs_bob" + private val bobPassword = "TopSecret_1234%Bobby" + private val charlieUser = "rs_charlie" + private val charliePassword = "TopSecret_1234%Charlie" + private var aliceClient: RestClient? = null + private var bobClient: RestClient? = null + private var charlieClient: RestClient? = null + + @Before + fun setupUsers() { + if (aliceClient != null) return + createNotificationsRole() + createUser(aliceUser, alicePassword, arrayOf("engineering")) + createUser(bobUser, bobPassword, arrayOf("marketing")) + createUser(charlieUser, charliePassword, arrayOf("sales")) + createUserRolesMapping(notificationsFullAccessRole, arrayOf(aliceUser, bobUser, charlieUser)) + + aliceClient = SecureRestClientBuilder(clusterHosts.toTypedArray(), isHttps(), aliceUser, alicePassword) + .setSocketTimeout(60000).build() + bobClient = SecureRestClientBuilder(clusterHosts.toTypedArray(), isHttps(), bobUser, bobPassword) + .setSocketTimeout(60000).build() + charlieClient = SecureRestClientBuilder(clusterHosts.toTypedArray(), isHttps(), charlieUser, charliePassword) + .setSocketTimeout(60000).build() + } + + @After + fun cleanupClients() { + aliceClient?.close() + bobClient?.close() + charlieClient?.close() + aliceClient = null + bobClient = null + charlieClient = null + } + + // --- Owner tests --- + + fun `test owner can perform all operations on their own config`() { + val configId = createConfig(configType = ConfigType.SLACK, client = aliceClient!!) + + // Owner can read + val getResponse = executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.OK.status, + aliceClient!! + ) + Assert.assertNotNull(getResponse) + + // Owner can update + executeRequest( + RestRequest.Method.PUT.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + buildUpdateJson("alice updated her own config"), + RestStatus.OK.status, + aliceClient!! + ) + + // Owner can share + shareResource(aliceClient!!, configId, "notifications_read_only", bobUser) + + // Owner can delete + executeRequest( + RestRequest.Method.DELETE.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.OK.status, + aliceClient!! + ) + } + + // --- Non-owner without sharing --- + + fun `test non-owner with full cluster role cannot access unshared config`() { + val configId = createConfig(configType = ConfigType.SLACK, client = aliceClient!!) + + // Bob has full cluster permissions but no resource-level access + executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.FORBIDDEN.status, + bobClient!! + ) + + // Bob cannot update + executeRequest( + RestRequest.Method.PUT.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + buildUpdateJson("attempted update"), + RestStatus.FORBIDDEN.status, + bobClient!! + ) + + // Bob cannot delete + executeRequest( + RestRequest.Method.DELETE.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.FORBIDDEN.status, + bobClient!! + ) + + // Bob cannot share a resource they have no access to + val shareRequest = Request("PUT", "/_plugins/_security/api/resource/share") + shareRequest.setJsonEntity(buildShareJson(configId, "notifications_read_only", charlieUser)) + executeRequest(shareRequest, RestStatus.FORBIDDEN.status, bobClient!!) + } + + // --- Isolation: DLS-based search filtering --- + + fun `test user only sees own and shared configs in list`() { + val aliceConfigId = createConfig(nameSubstring = "alice-config", configType = ConfigType.SLACK, client = aliceClient!!) + val bobConfigId = createConfig(nameSubstring = "bob-config", configType = ConfigType.SLACK, client = bobClient!!) + + // Alice should only see her own config + val aliceList = executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs", + "", + RestStatus.OK.status, + aliceClient!! + ) + val aliceConfigIds = extractConfigIds(aliceList) + Assert.assertTrue("Alice should see her own config", aliceConfigIds.contains(aliceConfigId)) + Assert.assertFalse("Alice should NOT see Bob's config", aliceConfigIds.contains(bobConfigId)) + + // Bob should only see his own config + val bobList = executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs", + "", + RestStatus.OK.status, + bobClient!! + ) + val bobConfigIds = extractConfigIds(bobList) + Assert.assertTrue("Bob should see his own config", bobConfigIds.contains(bobConfigId)) + Assert.assertFalse("Bob should NOT see Alice's config", bobConfigIds.contains(aliceConfigId)) + + // Share Alice's config with Bob + shareResource(aliceClient!!, aliceConfigId, "notifications_read_only", bobUser) + Thread.sleep(2000) + + // Now Bob should see both + val bobListAfterShare = executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs", + "", + RestStatus.OK.status, + bobClient!! + ) + val bobConfigIdsAfter = extractConfigIds(bobListAfterShare) + Assert.assertTrue("Bob should see his own config", bobConfigIdsAfter.contains(bobConfigId)) + Assert.assertTrue("Bob should see shared config from Alice", bobConfigIdsAfter.contains(aliceConfigId)) + } + + // --- read_only access level --- + + fun `test read_only access grants only read`() { + val configId = createConfig(configType = ConfigType.SLACK, client = aliceClient!!) + shareResource(aliceClient!!, configId, "notifications_read_only", bobUser) + Thread.sleep(2000) + + // Bob can read + val response = executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.OK.status, + bobClient!! + ) + Assert.assertNotNull(response) + + // Bob cannot update + executeRequest( + RestRequest.Method.PUT.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + buildUpdateJson("read_only user update attempt"), + RestStatus.FORBIDDEN.status, + bobClient!! + ) + + // Verify config is unchanged after failed update attempt + val afterFailedUpdate = executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.OK.status, + aliceClient!! + ) + val configName = afterFailedUpdate.get("config_list").asJsonArray[0].asJsonObject + .get("config").asJsonObject.get("name").asString + Assert.assertNotEquals("read_only user update attempt", configName) + + // Bob cannot delete + executeRequest( + RestRequest.Method.DELETE.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.FORBIDDEN.status, + bobClient!! + ) + + // Bob cannot share further + val shareRequest = Request("PUT", "/_plugins/_security/api/resource/share") + shareRequest.setJsonEntity(buildShareJson(configId, "notifications_read_only", charlieUser)) + executeRequest(shareRequest, RestStatus.FORBIDDEN.status, bobClient!!) + } + + // --- read_write access level --- + + fun `test read_write access grants read update and delete but not share`() { + val configId = createConfig(configType = ConfigType.SLACK, client = aliceClient!!) + shareResource(aliceClient!!, configId, "notifications_read_write", bobUser) + Thread.sleep(2000) + + // Bob can read + executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.OK.status, + bobClient!! + ) + + // Bob can update + executeRequest( + RestRequest.Method.PUT.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + buildUpdateJson("updated by bob"), + RestStatus.OK.status, + bobClient!! + ) + + // Alice sees Bob's update + val aliceGetResponse = executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.OK.status, + aliceClient!! + ) + val configName = aliceGetResponse.get("config_list").asJsonArray[0].asJsonObject + .get("config").asJsonObject.get("name").asString + Assert.assertEquals("updated by bob", configName) + + // Bob cannot share — read_write does not include share action + val shareRequest = Request("PUT", "/_plugins/_security/api/resource/share") + shareRequest.setJsonEntity(buildShareJson(configId, "notifications_read_only", charlieUser)) + executeRequest(shareRequest, RestStatus.FORBIDDEN.status, bobClient!!) + + // Bob can delete + executeRequest( + RestRequest.Method.DELETE.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.OK.status, + bobClient!! + ) + } + + // --- full_access level --- + + fun `test full_access grants all operations including share`() { + val configId = createConfig(configType = ConfigType.SLACK, client = aliceClient!!) + shareResource(aliceClient!!, configId, "notifications_full_access", bobUser) + Thread.sleep(2000) + + // Bob can read + executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.OK.status, + bobClient!! + ) + + // Bob can update + executeRequest( + RestRequest.Method.PUT.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + buildUpdateJson("full access update by bob"), + RestStatus.OK.status, + bobClient!! + ) + + // Alice sees Bob's update + val aliceGetResponse = executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.OK.status, + aliceClient!! + ) + val updatedName = aliceGetResponse.get("config_list").asJsonArray[0].asJsonObject + .get("config").asJsonObject.get("name").asString + Assert.assertEquals("full access update by bob", updatedName) + + // Bob can share further with charlie at read_only level + shareResource(bobClient!!, configId, "notifications_read_only", charlieUser) + Thread.sleep(2000) + + // Charlie can read (shared by bob, not the owner) + val charlieResponse = executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.OK.status, + charlieClient!! + ) + Assert.assertNotNull(charlieResponse) + + // Charlie cannot delete (read_only access) + executeRequest( + RestRequest.Method.DELETE.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.FORBIDDEN.status, + charlieClient!! + ) + + // Bob can delete (full_access) + executeRequest( + RestRequest.Method.DELETE.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.OK.status, + bobClient!! + ) + } + + // --- Revoke access --- + + fun `test revoking access removes permissions`() { + val configId = createConfig(configType = ConfigType.SLACK, client = aliceClient!!) + shareResource(aliceClient!!, configId, "notifications_read_only", bobUser) + Thread.sleep(2000) + + // Bob can read while shared + executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.OK.status, + bobClient!! + ) + + // Alice revokes Bob's access via PATCH with "revoke" field + val revokeRequest = Request("PATCH", "/_plugins/_security/api/resource/share") + revokeRequest.setJsonEntity( + """ + { + "resource_id": "$configId", + "resource_type": "notification_config", + "revoke": { + "notifications_read_only": { + "users": ["$bobUser"] + } + } + } + """.trimIndent() + ) + val revokeResponse = aliceClient!!.performRequest(revokeRequest) + Assert.assertEquals(200, revokeResponse.statusLine.statusCode) + Thread.sleep(2000) + + // Bob can no longer access + executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.FORBIDDEN.status, + bobClient!! + ) + } + + // --- Helpers --- + + private fun extractConfigIds(response: com.google.gson.JsonObject): List { + val ids = mutableListOf() + if (response.has("config_list")) { + response.getAsJsonArray("config_list").forEach { item -> + ids.add(item.asJsonObject.get("config_id").asString) + } + } + return ids + } + + private fun buildUpdateJson(name: String): String { + return """ + { + "config":{ + "name":"$name", + "description":"updated config", + "config_type":"slack", + "is_enabled":true, + "slack":{"url":"https://hooks.slack.com/services/updated_url"} + } + } + """.trimIndent() + } + + private fun buildShareJson(resourceId: String, accessLevel: String, user: String): String { + return """ + { + "resource_id": "$resourceId", + "resource_type": "notification_config", + "share_with": { + "$accessLevel": { + "users": ["$user"] + } + } + } + """.trimIndent() + } + + private fun shareResource(client: RestClient, resourceId: String, accessLevel: String, user: String) { + val request = Request("PUT", "/_plugins/_security/api/resource/share") + request.setJsonEntity(buildShareJson(resourceId, accessLevel, user)) + val response = client.performRequest(request) + Assert.assertEquals(200, response.statusLine.statusCode) + } + + private fun createNotificationsRole() { + val request = Request("PUT", "/_plugins/_security/api/roles/$notificationsFullAccessRole") + request.setJsonEntity( + """ + { + "cluster_permissions": [ + "cluster:admin/opensearch/notifications/*", + "cluster:admin/security/resource/*" + ], + "tenant_permissions": [] + } + """.trimIndent() + ) + adminClient().performRequest(request) + } +} diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SecurityNotificationIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SecurityNotificationIT.kt index 1ef313d17b8..5f2ffb36325 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SecurityNotificationIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SecurityNotificationIT.kt @@ -24,6 +24,8 @@ class SecurityNotificationIT : PluginRestTestCase() { fun setup() { // things to execute once and keep around for the class org.junit.Assume.assumeTrue(System.getProperty("https", "false")!!.toBoolean()) + // Skip these tests when resource sharing is enabled - ResourceSharingNotificationIT covers that path + org.junit.Assume.assumeFalse(System.getProperty("resource_sharing.enabled", "false")!!.toBoolean()) } } diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SlackNotificationAccessIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SlackNotificationAccessIT.kt index 1b4390858c6..07e0bc7ed99 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SlackNotificationAccessIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SlackNotificationAccessIT.kt @@ -26,8 +26,8 @@ class SlackNotificationAccessIT : PluginRestTestCase() { @BeforeClass @JvmStatic fun setup() { - // things to execute once and keep around for the class org.junit.Assume.assumeTrue(System.getProperty("https", "false")!!.toBoolean()) + org.junit.Assume.assumeFalse(System.getProperty("resource_sharing.enabled", "false")!!.toBoolean()) } } diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SmtpAccountAccessIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SmtpAccountAccessIT.kt index 6bee8cbb297..ffa25ed7914 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SmtpAccountAccessIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SmtpAccountAccessIT.kt @@ -28,8 +28,8 @@ class SmtpAccountAccessIT : PluginRestTestCase() { @BeforeClass @JvmStatic fun setup() { - // things to execute once and keep around for the class org.junit.Assume.assumeTrue(System.getProperty("https", "false")!!.toBoolean()) + org.junit.Assume.assumeFalse(System.getProperty("resource_sharing.enabled", "false")!!.toBoolean()) } } diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/CreateNotificationConfigIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/CreateNotificationConfigIT.kt index be7a8cfeb59..627cfaf46ec 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/CreateNotificationConfigIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/CreateNotificationConfigIT.kt @@ -314,7 +314,7 @@ class CreateNotificationConfigIT : PluginRestTestCase() { Assert.assertEquals(0, getCurrentMappingsSchemaVersion()) createConfig() - Assert.assertEquals(2, getCurrentMappingsSchemaVersion()) + Assert.assertEquals(3, getCurrentMappingsSchemaVersion()) } fun `test _meta field not exists in current mappings`() { @@ -366,6 +366,6 @@ class CreateNotificationConfigIT : PluginRestTestCase() { Assert.assertEquals(1, getCurrentMappingsSchemaVersion()) createConfig() - Assert.assertEquals(2, getCurrentMappingsSchemaVersion()) + Assert.assertEquals(3, getCurrentMappingsSchemaVersion()) } }