diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt b/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt index 3dc2cd571..0d3df167b 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/AlertingPlugin.kt @@ -432,7 +432,8 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R AlertingSettings.COMMENTS_MAX_CONTENT_SIZE, AlertingSettings.MAX_COMMENTS_PER_ALERT, AlertingSettings.MAX_COMMENTS_PER_NOTIFICATION, - AlertingSettings.NOTIFICATION_CONTEXT_RESULTS_ALLOWED_ROLES + AlertingSettings.NOTIFICATION_CONTEXT_RESULTS_ALLOWED_ROLES, + AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY ) } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt b/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt index 2ae09aead..91fe8745e 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/settings/AlertingSettings.kt @@ -302,5 +302,13 @@ class AlertingSettings { Setting.Property.NodeScope, Setting.Property.Dynamic ) + + val FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY = Setting.simpleString( + "plugins.alerting.filter_by_backend_roles_access_strategy", + FilterByBackendRolesAccessStrategy.INTERSECT.strategy, + FilterByBackendRolesAccessStrategyValidator(), + Setting.Property.NodeScope, + Setting.Property.Dynamic + ) } } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/settings/FilterByBackendRolesAccessStrategy.kt b/alerting/src/main/kotlin/org/opensearch/alerting/settings/FilterByBackendRolesAccessStrategy.kt new file mode 100644 index 000000000..b21d23187 --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/settings/FilterByBackendRolesAccessStrategy.kt @@ -0,0 +1,26 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.settings + +/** + * Defines the FilterByBackendRolesAccessStrategy + */ +enum class FilterByBackendRolesAccessStrategy(val strategy: String) { + /** + * User backend roles must contain all resource backend roles to have access + */ + ALL("all"), + + /** + * Backend roles must be exactly equal to have access + */ + EXACT("exact"), + + /** + * Backend roles must intersect to have access + */ + INTERSECT("intersect"), +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/settings/FilterByBackendRolesAccessStrategyValidator.kt b/alerting/src/main/kotlin/org/opensearch/alerting/settings/FilterByBackendRolesAccessStrategyValidator.kt new file mode 100644 index 000000000..0022f5fee --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/settings/FilterByBackendRolesAccessStrategyValidator.kt @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.settings + +import org.opensearch.common.settings.Setting + +class FilterByBackendRolesAccessStrategyValidator : Setting.Validator { + override fun validate(strategy: String) { + val allStrategies: List = FilterByBackendRolesAccessStrategy.entries.map { it.strategy } + + when (strategy) { + FilterByBackendRolesAccessStrategy.ALL.strategy, + FilterByBackendRolesAccessStrategy.EXACT.strategy, + FilterByBackendRolesAccessStrategy.INTERSECT.strategy -> {} + else -> throw IllegalArgumentException( + "Setting value must be one of [$allStrategies]" + ) + } + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/SecureTransportAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/SecureTransportAction.kt index 54667e125..564ae24be 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/SecureTransportAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/SecureTransportAction.kt @@ -8,6 +8,7 @@ package org.opensearch.alerting.transport import org.apache.logging.log4j.LogManager import org.opensearch.OpenSearchStatusException import org.opensearch.alerting.settings.AlertingSettings +import org.opensearch.alerting.settings.FilterByBackendRolesAccessStrategy import org.opensearch.cluster.service.ClusterService import org.opensearch.commons.ConfigConstants import org.opensearch.commons.alerting.util.AlertingException @@ -38,9 +39,13 @@ private val log = LogManager.getLogger(SecureTransportAction::class.java) interface SecureTransportAction { var filterByEnabled: Boolean + var filterByAccessStrategy: String fun listenFilterBySettingChange(clusterService: ClusterService) { clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.FILTER_BY_BACKEND_ROLES) { filterByEnabled = it } + clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY) { + filterByAccessStrategy = it + } } fun readUserFromThreadContext(client: Client): User? { @@ -100,6 +105,21 @@ interface SecureTransportAction { return true } + fun doUserBackendRolesMatchResource(userBackendRoles: List, resourceBackendRoles: List): Boolean { + if (filterByAccessStrategy == FilterByBackendRolesAccessStrategy.ALL.strategy) { + return userBackendRoles.containsAll(resourceBackendRoles) + } else if (filterByAccessStrategy == FilterByBackendRolesAccessStrategy.INTERSECT.strategy) { + return resourceBackendRoles.any { it in userBackendRoles } + } else if (filterByAccessStrategy == FilterByBackendRolesAccessStrategy.EXACT.strategy) { + return resourceBackendRoles.sorted().equals(userBackendRoles.sorted()) + } + // Not sure if this is necessary, since there is a validator + // on the setting itself + throw IllegalArgumentException( + "Invalid filter by access strategy: $filterByAccessStrategy" + ) + } + /** * If FilterBy is enabled, this function verifies that the requester user has FilterBy permissions to access * the resource. If FilterBy is disabled, we will assume the user has permissions and return true. @@ -122,7 +142,7 @@ interface SecureTransportAction { if ( resourceBackendRoles == null || requesterBackendRoles == null || - resourceBackendRoles.intersect(requesterBackendRoles).isEmpty() + !doUserBackendRolesMatchResource(requesterBackendRoles, resourceBackendRoles) ) { actionListener.onFailure( AlertingException.wrap( diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt index 09f8d2e00..bdc6fd817 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteAlertingCommentAction.kt @@ -60,6 +60,7 @@ class TransportDeleteAlertingCommentAction @Inject constructor( @Volatile private var alertingCommentsEnabled = AlertingSettings.ALERTING_COMMENTS_ENABLED.get(settings) @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByAccessStrategy = AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY.get(settings) init { clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.ALERTING_COMMENTS_ENABLED) { diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteMonitorAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteMonitorAction.kt index b28311bd0..15567b381 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteMonitorAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteMonitorAction.kt @@ -56,6 +56,7 @@ class TransportDeleteMonitorAction @Inject constructor( SecureTransportAction { @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByAccessStrategy = AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY.get(settings) init { listenFilterBySettingChange(clusterService) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteWorkflowAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteWorkflowAction.kt index bf0d44eab..cb976ed61 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteWorkflowAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteWorkflowAction.kt @@ -83,6 +83,7 @@ class TransportDeleteWorkflowAction @Inject constructor( private val log = LogManager.getLogger(javaClass) @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByAccessStrategy = AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY.get(settings) init { listenFilterBySettingChange(clusterService) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDocLevelMonitorFanOutAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDocLevelMonitorFanOutAction.kt index 2e71cf5af..c49c76f01 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDocLevelMonitorFanOutAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDocLevelMonitorFanOutAction.kt @@ -196,6 +196,7 @@ class TransportDocLevelMonitorFanOutAction @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByAccessStrategy = AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY.get(settings) override fun doExecute( task: Task, diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetAlertsAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetAlertsAction.kt index 1fc3ef83a..0495af8b9 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetAlertsAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetAlertsAction.kt @@ -73,6 +73,7 @@ class TransportGetAlertsAction @Inject constructor( @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByAccessStrategy = AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY.get(settings) init { listenFilterBySettingChange(clusterService) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetDestinationsAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetDestinationsAction.kt index 60e5edb9d..bb138d00f 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetDestinationsAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetDestinationsAction.kt @@ -57,6 +57,7 @@ class TransportGetDestinationsAction @Inject constructor( SecureTransportAction { @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByAccessStrategy = AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY.get(settings) init { listenFilterBySettingChange(clusterService) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetFindingsAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetFindingsAction.kt index c4a1f2dbb..31fd2b7df 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetFindingsAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetFindingsAction.kt @@ -71,6 +71,7 @@ class TransportGetFindingsSearchAction @Inject constructor( SecureTransportAction { @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByAccessStrategy = AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY.get(settings) init { listenFilterBySettingChange(clusterService) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetMonitorAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetMonitorAction.kt index 97e65ec50..17a732ac8 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetMonitorAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetMonitorAction.kt @@ -69,6 +69,7 @@ class TransportGetMonitorAction @Inject constructor( @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByAccessStrategy = AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY.get(settings) init { listenFilterBySettingChange(clusterService) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetRemoteIndexesAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetRemoteIndexesAction.kt index b106903a8..02f110fc5 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetRemoteIndexesAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetRemoteIndexesAction.kt @@ -61,6 +61,7 @@ class TransportGetRemoteIndexesAction @Inject constructor( SecureTransportAction { @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByAccessStrategy = AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY.get(settings) @Volatile private var remoteMonitoringEnabled = CROSS_CLUSTER_MONITORING_ENABLED.get(settings) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetWorkflowAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetWorkflowAction.kt index d0d3e45f7..11a0dd86d 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetWorkflowAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetWorkflowAction.kt @@ -48,6 +48,7 @@ class TransportGetWorkflowAction @Inject constructor( private val log = LogManager.getLogger(javaClass) @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByAccessStrategy = AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY.get(settings) init { listenFilterBySettingChange(clusterService) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetWorkflowAlertsAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetWorkflowAlertsAction.kt index dd26bf032..c0c9633b5 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetWorkflowAlertsAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetWorkflowAlertsAction.kt @@ -69,6 +69,9 @@ class TransportGetWorkflowAlertsAction @Inject constructor( @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile + override var filterByAccessStrategy = AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY.get(settings) + @Volatile private var isAlertHistoryEnabled = AlertingSettings.ALERT_HISTORY_ENABLED.get(settings) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt index 8592c505c..6b912f0cc 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexAlertingCommentAction.kt @@ -86,6 +86,7 @@ constructor( @Volatile private var indexTimeout = INDEX_TIMEOUT.get(settings) @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByAccessStrategy = AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY.get(settings) init { clusterService.clusterSettings.addSettingsUpdateConsumer(ALERTING_COMMENTS_ENABLED) { alertingCommentsEnabled = it } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexMonitorAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexMonitorAction.kt index c36fc4957..a9ae56c54 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexMonitorAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexMonitorAction.kt @@ -111,6 +111,7 @@ class TransportIndexMonitorAction @Inject constructor( @Volatile private var maxActionThrottle = MAX_ACTION_THROTTLE_VALUE.get(settings) @Volatile private var allowList = ALLOW_LIST.get(settings) @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByAccessStrategy = AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY.get(settings) init { clusterService.clusterSettings.addSettingsUpdateConsumer(ALERTING_MAX_MONITORS) { maxMonitors = it } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexWorkflowAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexWorkflowAction.kt index 3c80af129..1d3ef6663 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexWorkflowAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexWorkflowAction.kt @@ -119,6 +119,9 @@ class TransportIndexWorkflowAction @Inject constructor( @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile + override var filterByAccessStrategy = AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY.get(settings) + init { clusterService.clusterSettings.addSettingsUpdateConsumer(ALERTING_MAX_MONITORS) { maxMonitors = it } clusterService.clusterSettings.addSettingsUpdateConsumer(REQUEST_TIMEOUT) { requestTimeout = it } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt index 9e8c3d153..f41a9b6de 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchAlertingCommentAction.kt @@ -63,6 +63,8 @@ class TransportSearchAlertingCommentAction @Inject constructor( @Volatile private var alertingCommentsEnabled = AlertingSettings.ALERTING_COMMENTS_ENABLED.get(settings) @Volatile override var filterByEnabled: Boolean = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByAccessStrategy: String = AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY.get(settings) + init { clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.ALERTING_COMMENTS_ENABLED) { alertingCommentsEnabled = it diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchMonitorAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchMonitorAction.kt index 21c633553..83323f5e1 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchMonitorAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchMonitorAction.kt @@ -62,6 +62,9 @@ class TransportSearchMonitorAction @Inject constructor( SecureTransportAction { @Volatile override var filterByEnabled: Boolean = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile + override var filterByAccessStrategy: String = AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY.get(settings) + init { listenFilterBySettingChange(clusterService) } diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt b/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt index 70a3d49bd..f4426ecfe 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt @@ -1482,6 +1482,20 @@ abstract class AlertingRestTestCase : ODFERestTestCase() { assertEquals(updateResponse.statusLine.toString(), 200, updateResponse.statusLine.statusCode) } + fun setFilterByBackendRolesStrategy(strategy: String) { + val updateResponse = client().makeRequest( + "PUT", "_cluster/settings", + emptyMap(), + StringEntity( + XContentFactory.jsonBuilder().startObject().field("persistent") + .startObject().field(AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY.key, strategy).endObject() + .endObject().string(), + ContentType.APPLICATION_JSON + ) + ) + assertEquals(updateResponse.statusLine.toString(), 200, updateResponse.statusLine.statusCode) + } + fun disableFilterBy() { val updateResponse = client().makeRequest( "PUT", "_cluster/settings", diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureMonitorRestApiIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureMonitorRestApiIT.kt index 2f7fca92e..bba6d6b4c 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureMonitorRestApiIT.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureMonitorRestApiIT.kt @@ -323,6 +323,277 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { } } + fun `test get monitor succeeds for same backend roles in same order when filterByAccessStrategy is exact`() { + enableFilterBy() + if (!isHttps()) { + // if security is disabled and filter by is enabled, we can't create monitor + // refer: `test create monitor with enable filter by` + return + } + setFilterByBackendRolesStrategy("exact") + + val monitor = randomQueryLevelMonitor(enabled = true) + + createUserWithRoles( + user, + listOf(ALERTING_FULL_ACCESS_ROLE, READALL_AND_MONITOR_ROLE), + listOf(TEST_HR_BACKEND_ROLE, "role2"), + false + ) + + val createdMonitor = createMonitorWithClient(userClient!!, monitor = monitor, listOf(TEST_HR_BACKEND_ROLE, "role2")) + assertNotNull("The monitor was not created", createdMonitor) + + createUserRolesMapping(ALERTING_FULL_ACCESS_ROLE, arrayOf()) + createUserRolesMapping(READALL_AND_MONITOR_ROLE, arrayOf()) + + // getUser should have access to the monitor + val getUser = "getUser" + createUserWithTestDataAndCustomRole( + getUser, + TEST_HR_INDEX, + TEST_HR_ROLE, + listOf(TEST_HR_BACKEND_ROLE, "role2"), + getClusterPermissionsFromCustomRole(ALERTING_GET_MONITOR_ACCESS) + ) + val getUserClient = SecureRestClientBuilder(clusterHosts.toTypedArray(), isHttps(), getUser, password) + .setSocketTimeout(60000) + .setConnectionRequestTimeout(180000) + .build() + + try { + val getMonitorResponse = getUserClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${createdMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + assertEquals("Get monitor failed", RestStatus.OK, getMonitorResponse?.restStatus()) + } finally { + deleteRoleAndRoleMapping(TEST_HR_ROLE) + deleteUser(getUser) + getUserClient?.close() + } + } + + fun `test get monitor succeeds for same backend roles in different order when filterByAccessStrategy is exact`() { + enableFilterBy() + if (!isHttps()) { + // if security is disabled and filter by is enabled, we can't create monitor + // refer: `test create monitor with enable filter by` + return + } + setFilterByBackendRolesStrategy("exact") + + val monitor = randomQueryLevelMonitor(enabled = true) + + createUserWithRoles( + user, + listOf(ALERTING_FULL_ACCESS_ROLE, READALL_AND_MONITOR_ROLE), + listOf(TEST_HR_BACKEND_ROLE, "role2"), + false + ) + + val createdMonitor = createMonitorWithClient(userClient!!, monitor = monitor, listOf(TEST_HR_BACKEND_ROLE, "role2")) + assertNotNull("The monitor was not created", createdMonitor) + + createUserRolesMapping(ALERTING_FULL_ACCESS_ROLE, arrayOf()) + createUserRolesMapping(READALL_AND_MONITOR_ROLE, arrayOf()) + + // getUser should have access to the monitor + val getUser = "getUser" + createUserWithTestDataAndCustomRole( + getUser, + TEST_HR_INDEX, + TEST_HR_ROLE, + // intentionally change order of backend roles from order on monitor to ensure order doesn't matter + listOf("role2", TEST_HR_BACKEND_ROLE), + getClusterPermissionsFromCustomRole(ALERTING_GET_MONITOR_ACCESS) + ) + val getUserClient = SecureRestClientBuilder(clusterHosts.toTypedArray(), isHttps(), getUser, password) + .setSocketTimeout(60000) + .setConnectionRequestTimeout(180000) + .build() + + try { + val getMonitorResponse = getUserClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${createdMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + assertEquals("Get monitor failed", RestStatus.OK, getMonitorResponse?.restStatus()) + } finally { + deleteRoleAndRoleMapping(TEST_HR_ROLE) + deleteUser(getUser) + getUserClient?.close() + } + } + + fun `test get monitor fails for different backend roles when filterByAccessStrategy is exact`() { + enableFilterBy() + if (!isHttps()) { + // if security is disabled and filter by is enabled, we can't create monitor + // refer: `test create monitor with enable filter by` + return + } + setFilterByBackendRolesStrategy("exact") + + val monitor = randomQueryLevelMonitor(enabled = true) + + createUserWithRoles( + user, + listOf(ALERTING_FULL_ACCESS_ROLE, READALL_AND_MONITOR_ROLE), + listOf(TEST_HR_BACKEND_ROLE, "role2"), + false + ) + + val createdMonitor = createMonitorWithClient(userClient!!, monitor = monitor, listOf(TEST_HR_BACKEND_ROLE, "role2")) + assertNotNull("The monitor was not created", createdMonitor) + + createUserRolesMapping(ALERTING_FULL_ACCESS_ROLE, arrayOf()) + createUserRolesMapping(READALL_AND_MONITOR_ROLE, arrayOf()) + + // getUser should NOT have access to the monitor + val getUser = "getUser" + createUserWithTestDataAndCustomRole( + getUser, + TEST_HR_INDEX, + TEST_HR_ROLE, + // roles are different from monitor backend roles specified above + listOf(TEST_HR_BACKEND_ROLE, "role1"), + getClusterPermissionsFromCustomRole(ALERTING_GET_MONITOR_ACCESS) + ) + val getUserClient = SecureRestClientBuilder(clusterHosts.toTypedArray(), isHttps(), getUser, password) + .setSocketTimeout(60000) + .setConnectionRequestTimeout(180000) + .build() + + try { + val getMonitorResponse = getUserClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${createdMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + fail("Expected Forbidden exception") + } catch (e: ResponseException) { + assertEquals("Get monitor failed", RestStatus.FORBIDDEN.status, e.response.statusLine.statusCode) + } finally { + deleteRoleAndRoleMapping(TEST_HR_ROLE) + deleteUser(getUser) + getUserClient?.close() + } + } + + fun `test get monitor succeeds when filterByAccessStrategy is all and user backend roles contain all resource roles`() { + enableFilterBy() + if (!isHttps()) { + // if security is disabled and filter by is enabled, we can't create monitor + // refer: `test create monitor with enable filter by` + return + } + setFilterByBackendRolesStrategy("all") + + val monitor = randomQueryLevelMonitor(enabled = true) + + createUserWithRoles( + user, + listOf(ALERTING_FULL_ACCESS_ROLE, READALL_AND_MONITOR_ROLE), + listOf("role1", "role2"), + false + ) + + val createdMonitor = createMonitorWithClient(userClient!!, monitor = monitor, listOf("role1", "role2")) + assertNotNull("The monitor was not created", createdMonitor) + + createUserRolesMapping(ALERTING_FULL_ACCESS_ROLE, arrayOf()) + createUserRolesMapping(READALL_AND_MONITOR_ROLE, arrayOf()) + + // getUser should have access to the monitor + val getUser = "getUser" + createUserWithTestDataAndCustomRole( + getUser, + TEST_HR_INDEX, + TEST_HR_ROLE, + listOf("role3", "role2", "role1"), + getClusterPermissionsFromCustomRole(ALERTING_GET_MONITOR_ACCESS) + ) + val getUserClient = SecureRestClientBuilder(clusterHosts.toTypedArray(), isHttps(), getUser, password) + .setSocketTimeout(60000) + .setConnectionRequestTimeout(180000) + .build() + + try { + val getMonitorResponse = getUserClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${createdMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + assertEquals("Get monitor failed", RestStatus.OK, getMonitorResponse?.restStatus()) + } finally { + deleteRoleAndRoleMapping(TEST_HR_ROLE) + deleteUser(getUser) + getUserClient?.close() + } + } + + fun `test get monitor fails when filterByAccessStrategy is all and user backend roles do not contain all resource roles`() { + enableFilterBy() + if (!isHttps()) { + // if security is disabled and filter by is enabled, we can't create monitor + // refer: `test create monitor with enable filter by` + return + } + setFilterByBackendRolesStrategy("all") + + val monitor = randomQueryLevelMonitor(enabled = true) + + createUserWithRoles( + user, + listOf(ALERTING_FULL_ACCESS_ROLE, READALL_AND_MONITOR_ROLE), + listOf("role1", "role2"), + false + ) + + val createdMonitor = createMonitorWithClient(userClient!!, monitor = monitor, listOf("role1", "role2")) + assertNotNull("The monitor was not created", createdMonitor) + + createUserRolesMapping(ALERTING_FULL_ACCESS_ROLE, arrayOf()) + createUserRolesMapping(READALL_AND_MONITOR_ROLE, arrayOf()) + + // getUser should have access to the monitor + val getUser = "getUser" + createUserWithTestDataAndCustomRole( + getUser, + TEST_HR_INDEX, + TEST_HR_ROLE, + listOf("role2", "role3"), + getClusterPermissionsFromCustomRole(ALERTING_GET_MONITOR_ACCESS) + ) + val getUserClient = SecureRestClientBuilder(clusterHosts.toTypedArray(), isHttps(), getUser, password) + .setSocketTimeout(60000) + .setConnectionRequestTimeout(180000) + .build() + + try { + val getMonitorResponse = getUserClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${createdMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + fail("Expected Forbidden exception") + } catch (e: ResponseException) { + assertEquals("Get monitor failed", RestStatus.FORBIDDEN.status, e.response.statusLine.statusCode) + } finally { + deleteRoleAndRoleMapping(TEST_HR_ROLE) + deleteUser(getUser) + getUserClient?.close() + } + } + fun getDocs(response: Response?): Any? { val hits = createParser( XContentType.JSON.xContent(), diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/settings/AlertingSettingsTests.kt b/alerting/src/test/kotlin/org/opensearch/alerting/settings/AlertingSettingsTests.kt index 6ee8c4997..011f685f8 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/settings/AlertingSettingsTests.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/settings/AlertingSettingsTests.kt @@ -80,6 +80,7 @@ class AlertingSettingsTests : OpenSearchTestCase() { AlertingSettings.REQUEST_TIMEOUT, AlertingSettings.MAX_ACTION_THROTTLE_VALUE, AlertingSettings.FILTER_BY_BACKEND_ROLES, + AlertingSettings.FILTER_BY_BACKEND_ROLES_ACCESS_STRATEGY, ScheduledJobSettings.SWEEP_PERIOD, ScheduledJobSettings.SWEEP_PAGE_SIZE, ScheduledJobSettings.SWEEP_BACKOFF_RETRY_COUNT, diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/settings/FilterByBackendRolesAccessStrategyValidatorTests.kt b/alerting/src/test/kotlin/org/opensearch/alerting/settings/FilterByBackendRolesAccessStrategyValidatorTests.kt new file mode 100644 index 000000000..9082fc224 --- /dev/null +++ b/alerting/src/test/kotlin/org/opensearch/alerting/settings/FilterByBackendRolesAccessStrategyValidatorTests.kt @@ -0,0 +1,26 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.alerting.settings + +import org.opensearch.test.OpenSearchTestCase +import kotlin.test.assertFailsWith + +class FilterByBackendRolesAccessStrategyValidatorTests : OpenSearchTestCase() { + + fun `test accepts valid strategies`() { + val validator = FilterByBackendRolesAccessStrategyValidator() + validator.validate("all") + validator.validate("exact") + validator.validate("intersect") + } + + fun `test rejects invalid strategy`() { + val validator = FilterByBackendRolesAccessStrategyValidator() + assertFailsWith(IllegalArgumentException::class, "Expected IllegalArgumentException") { + validator.validate("invalid") + } + } +}