From 1d0e8fff702529e7a717c5d76e6260e6843643ca Mon Sep 17 00:00:00 2001 From: Sriram <59816283+skkosuri-amzn@users.noreply.github.com> Date: Wed, 14 Oct 2020 12:30:35 -0700 Subject: [PATCH] Change AlertError message and remove deny-list destinations check during monitor creation (#270) * Change AlertError message and remove deny-list destinations during monitor creation * Removed experimental test code * Comment dead code * Fix async flow for monitor create --- .../alerting/model/MonitorRunResult.kt | 8 +- .../transport/TransportIndexMonitorAction.kt | 80 ++++++++++-------- .../alerting/MonitorRunnerIT.kt | 2 +- .../alerting/resthandler/MonitorRestApiIT.kt | 9 +- core/libs/common-utils-1.10.1.0.jar | Bin 16869 -> 16868 bytes 5 files changed, 50 insertions(+), 49 deletions(-) diff --git a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/MonitorRunResult.kt b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/MonitorRunResult.kt index d5292a91..1be633e8 100644 --- a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/MonitorRunResult.kt +++ b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/MonitorRunResult.kt @@ -61,11 +61,11 @@ data class MonitorRunResult( /** Returns error information to store in the Alert. Currently it's just the stack trace but it can be more */ fun alertError(): AlertError? { if (error != null) { - return AlertError(Instant.now(), "Error running monitor:\n${error.userErrorMessage()}") + return AlertError(Instant.now(), "Failed running monitor:\n${error.userErrorMessage()}") } if (inputResults.error != null) { - return AlertError(Instant.now(), "Error fetching inputs:\n${inputResults.error.userErrorMessage()}") + return AlertError(Instant.now(), "Failed fetching inputs:\n${inputResults.error.userErrorMessage()}") } return null } @@ -164,11 +164,11 @@ data class TriggerRunResult( /** Returns error information to store in the Alert. Currently it's just the stack trace but it can be more */ fun alertError(): AlertError? { if (error != null) { - return AlertError(Instant.now(), "Error evaluating trigger:\n${error.userErrorMessage()}") + return AlertError(Instant.now(), "Failed evaluating trigger:\n${error.userErrorMessage()}") } for (actionResult in actionResults.values) { if (actionResult.error != null) { - return AlertError(Instant.now(), "Error running action:\n${actionResult.error.userErrorMessage()}") + return AlertError(Instant.now(), "Failed running action:\n${actionResult.error.userErrorMessage()}") } } return null diff --git a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/transport/TransportIndexMonitorAction.kt b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/transport/TransportIndexMonitorAction.kt index c096620c..199ae89b 100644 --- a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/transport/TransportIndexMonitorAction.kt +++ b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/transport/TransportIndexMonitorAction.kt @@ -24,7 +24,6 @@ import com.amazon.opendistroforelasticsearch.alerting.core.model.ScheduledJob.Co import com.amazon.opendistroforelasticsearch.alerting.core.model.ScheduledJob.Companion.SCHEDULED_JOB_TYPE import com.amazon.opendistroforelasticsearch.alerting.core.model.SearchInput import com.amazon.opendistroforelasticsearch.alerting.model.Monitor -import com.amazon.opendistroforelasticsearch.alerting.model.destination.Destination import com.amazon.opendistroforelasticsearch.alerting.settings.AlertingSettings.Companion.ALERTING_MAX_MONITORS import com.amazon.opendistroforelasticsearch.alerting.settings.AlertingSettings.Companion.INDEX_TIMEOUT import com.amazon.opendistroforelasticsearch.alerting.settings.AlertingSettings.Companion.MAX_ACTION_THROTTLE_VALUE @@ -35,6 +34,7 @@ import com.amazon.opendistroforelasticsearch.alerting.util.IndexUtils import com.amazon.opendistroforelasticsearch.commons.authuser.User import com.amazon.opendistroforelasticsearch.commons.authuser.AuthUserRequestBuilder import org.apache.logging.log4j.LogManager +import org.elasticsearch.ElasticsearchSecurityException import org.elasticsearch.ElasticsearchStatusException import org.elasticsearch.action.ActionListener import org.elasticsearch.action.admin.indices.create.CreateIndexResponse @@ -100,13 +100,46 @@ class TransportIndexMonitorAction @Inject constructor( } override fun doExecute(task: Task, request: IndexMonitorRequest, actionListener: ActionListener) { + checkIndicesAndExecute(client, actionListener, request) + } - if (!isValidIndex(request, actionListener)) - return - - client.threadPool().threadContext.stashContext().use { - IndexMonitorHandler(client, actionListener, request).resolveUserAndStart() + /** + * Check if user has permissions to read the configured indices on the monitor and + * then create monitor. + */ + fun checkIndicesAndExecute( + client: Client, + actionListener: ActionListener, + request: IndexMonitorRequest + ) { + val indices = mutableListOf() + val searchInputs = request.monitor.inputs.filter { it.name() == SearchInput.SEARCH_FIELD } + searchInputs.forEach { + val searchInput = it as SearchInput + indices.addAll(searchInput.indices) } + val searchRequest = SearchRequest().indices(*indices.toTypedArray()) + .source(SearchSourceBuilder.searchSource().size(1).query(QueryBuilders.matchAllQuery())) + client.search(searchRequest, object : ActionListener { + override fun onResponse(searchResponse: SearchResponse) { + // User has read access to configured indices in the monitor, now create monitor with out user context. + client.threadPool().threadContext.stashContext().use { + IndexMonitorHandler(client, actionListener, request).resolveUserAndStart() + } + } + + // Due to below issue with security plugin, we get security_exception when invalid index name is mentioned. + // https://github.com/opendistro-for-elasticsearch/security/issues/718 + override fun onFailure(t: Exception) { + actionListener.onFailure(AlertingException.wrap( + when (t is ElasticsearchSecurityException) { + true -> ElasticsearchStatusException("User doesn't have read permissions for one or more configured index " + + "${indices.indices}", RestStatus.FORBIDDEN) + false -> t + } + )) + } + }) } inner class IndexMonitorHandler( @@ -175,7 +208,8 @@ class TransportIndexMonitorAction @Inject constructor( */ private fun prepareMonitorIndexing() { - checkForDisallowedDestinations(allowList) + // Below check needs to be async operations and needs to be refactored issue#269 + // checkForDisallowedDestinations(allowList) try { validateActionThrottle(request.monitor, maxActionThrottle, TimeValue.timeValueMinutes(1)) @@ -348,7 +382,7 @@ class TransportIndexMonitorAction @Inject constructor( return null } - private fun checkForDisallowedDestinations(allowList: List) { + /*private fun checkForDisallowedDestinations(allowList: List) { this.request.monitor.triggers.forEach { trigger -> trigger.actions.forEach { action -> // Check for empty destinationId for test cases, otherwise we get test failures @@ -380,34 +414,6 @@ class TransportIndexMonitorAction @Inject constructor( } }) } - } - } - - /** - * Check if user has permissions to read the configured indices on the monitor. - * Due to below issue with security plugin, we get security_exception when invalid index name is mentioned. - * https://github.com/opendistro-for-elasticsearch/security/issues/718 - */ - private fun isValidIndex(request: IndexMonitorRequest, actionListener: ActionListener): Boolean { - var ret = true - val searchInputs = request.monitor.inputs.filter { it.name() == SearchInput.SEARCH_FIELD } - searchInputs.forEach { - val searchInput = it as SearchInput - val searchRequest = SearchRequest().indices(*searchInput.indices.toTypedArray()) - .source(SearchSourceBuilder.searchSource().size(1).query(QueryBuilders.matchAllQuery())) - client.search(searchRequest, object : ActionListener { - override fun onResponse(searchResponse: SearchResponse) { - // ignore - } - - override fun onFailure(t: Exception) { - val ex = ElasticsearchStatusException("User doesn't have read permissions for the configured index " + - "${searchInput.indices}", RestStatus.FORBIDDEN) - actionListener.onFailure(AlertingException.wrap(ex)) - ret = false - } - }) - } - return ret + }*/ } } diff --git a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/MonitorRunnerIT.kt b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/MonitorRunnerIT.kt index 616091b3..5c46c6e6 100644 --- a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/MonitorRunnerIT.kt +++ b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/MonitorRunnerIT.kt @@ -421,7 +421,7 @@ class MonitorRunnerIT : AlertingRestTestCase() { verifyAlert(errorAlert, monitor, ERROR) executeMonitor(monitor.id) assertEquals("Error does not match", - "Error evaluating trigger:\nparam[0]; return true\n^---- HERE", errorAlert.errorMessage) + "Failed evaluating trigger:\nparam[0]; return true\n^---- HERE", errorAlert.errorMessage) } fun `test execute monitor limits alert error history to 10 error messages`() { diff --git a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/resthandler/MonitorRestApiIT.kt b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/resthandler/MonitorRestApiIT.kt index 1ce940a7..4db1e976 100644 --- a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/resthandler/MonitorRestApiIT.kt +++ b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/resthandler/MonitorRestApiIT.kt @@ -25,17 +25,12 @@ import com.amazon.opendistroforelasticsearch.alerting.makeRequest import com.amazon.opendistroforelasticsearch.alerting.model.Alert import com.amazon.opendistroforelasticsearch.alerting.model.Monitor import com.amazon.opendistroforelasticsearch.alerting.model.Trigger -import com.amazon.opendistroforelasticsearch.alerting.model.destination.Chime -import com.amazon.opendistroforelasticsearch.alerting.model.destination.Destination import com.amazon.opendistroforelasticsearch.alerting.randomAction import com.amazon.opendistroforelasticsearch.alerting.randomAlert import com.amazon.opendistroforelasticsearch.alerting.randomMonitor import com.amazon.opendistroforelasticsearch.alerting.randomThrottle import com.amazon.opendistroforelasticsearch.alerting.randomTrigger -import com.amazon.opendistroforelasticsearch.alerting.randomUser import com.amazon.opendistroforelasticsearch.alerting.settings.AlertingSettings -import com.amazon.opendistroforelasticsearch.alerting.settings.DestinationSettings -import com.amazon.opendistroforelasticsearch.alerting.util.DestinationType import org.apache.http.HttpHeaders import org.apache.http.entity.ContentType import org.apache.http.message.BasicHeader @@ -53,7 +48,6 @@ import org.elasticsearch.search.builder.SearchSourceBuilder import org.elasticsearch.test.ESTestCase import org.elasticsearch.test.junit.annotations.TestLogging import org.elasticsearch.test.rest.ESRestTestCase -import java.time.Instant import java.time.ZoneId import java.time.temporal.ChronoUnit @@ -163,6 +157,7 @@ class MonitorRestApiIT : AlertingRestTestCase() { } } + /* Enable this test case after issue issue#269 is fixed. fun `test creating a monitor with a disallowed destination type fails`() { try { // Create a Chime Destination @@ -189,7 +184,7 @@ class MonitorRestApiIT : AlertingRestTestCase() { } catch (e: ResponseException) { assertEquals("Unexpected status", RestStatus.FORBIDDEN, e.response.restStatus()) } - } + }*/ @Throws(Exception::class) fun `test updating search for a monitor`() { diff --git a/core/libs/common-utils-1.10.1.0.jar b/core/libs/common-utils-1.10.1.0.jar index e92689107f48879cab902a6b53650149df85140e..5327060d89135b0d49e1c83f7f0af5aedc1c85fd 100644 GIT binary patch delta 5693 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 xk~dIwDJZq(cx9kVZgx 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