From ec87c8105ec610a3b4d27f4524037ec13d46abf7 Mon Sep 17 00:00:00 2001 From: Ashish Agrawal Date: Mon, 8 Feb 2021 10:20:45 -0800 Subject: [PATCH 1/6] Support SNS as a destination --- .../alerting/AlertingPlugin.kt | 5 +- .../alerting/MonitorRunner.kt | 3 + .../alerting/model/destination/Destination.kt | 25 ++- .../alerting/model/destination/SNS.kt | 39 ++++- .../alerting/settings/AWSSettings.kt | 51 ++++++ .../alerting/util/DestinationType.kt | 5 + .../plugin-metadata/plugin-security.policy | 24 +++ .../alerting/AlertingRestTestCase.kt | 1 + .../alerting/MonitorRunnerIT.kt | 1 + .../action/GetDestinationsResponseTests.kt | 1 + .../action/IndexDestinationRequestTests.kt | 2 + .../action/IndexDestinationResponseTests.kt | 2 +- .../alerting/model/DestinationTests.kt | 67 ++++++- .../resthandler/DestinationRestApiIT.kt | 9 + .../alerting/resthandler/SNSRestApiIT.kt | 65 +++++++ .../resthandler/SecureDestinationRestApiIT.kt | 8 + build.gradle | 1 + .../alerting/elasticapi/ElasticExtensions.kt | 7 + notification/build.gradle | 12 +- .../factory/DestinationFactoryProvider.java | 1 + .../factory/SNSDestinationFactory.java | 90 ++++++++++ .../destination/message/DestinationType.java | 2 +- .../destination/message/SNSMessage.java | 163 ++++++++++++++++++ .../alerting/destination/util/Util.java | 45 +++++ .../destination/SNSDestinationTest.java | 156 +++++++++++++++++ .../alerting/destination/UtilTest.java | 62 +++++++ 26 files changed, 832 insertions(+), 15 deletions(-) create mode 100644 alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/settings/AWSSettings.kt create mode 100644 alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/resthandler/SNSRestApiIT.kt create mode 100644 notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/factory/SNSDestinationFactory.java create mode 100644 notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/message/SNSMessage.java create mode 100644 notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/util/Util.java create mode 100644 notification/src/test/java/com/amazon/opendistroforelasticsearch/alerting/destination/SNSDestinationTest.java create mode 100644 notification/src/test/java/com/amazon/opendistroforelasticsearch/alerting/destination/UtilTest.java diff --git a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/AlertingPlugin.kt b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/AlertingPlugin.kt index bbca72e6..349c8536 100644 --- a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/AlertingPlugin.kt +++ b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/AlertingPlugin.kt @@ -62,6 +62,7 @@ import com.amazon.opendistroforelasticsearch.alerting.resthandler.RestSearchEmai import com.amazon.opendistroforelasticsearch.alerting.resthandler.RestSearchEmailGroupAction import com.amazon.opendistroforelasticsearch.alerting.resthandler.RestSearchMonitorAction import com.amazon.opendistroforelasticsearch.alerting.script.TriggerScript +import com.amazon.opendistroforelasticsearch.alerting.settings.AWSSettings import com.amazon.opendistroforelasticsearch.alerting.settings.AlertingSettings import com.amazon.opendistroforelasticsearch.alerting.settings.DestinationSettings import com.amazon.opendistroforelasticsearch.alerting.transport.TransportAcknowledgeAlertAction @@ -254,7 +255,9 @@ internal class AlertingPlugin : PainlessExtension, ActionPlugin, ScriptPlugin, R AlertingSettings.FILTER_BY_BACKEND_ROLES, DestinationSettings.EMAIL_USERNAME, DestinationSettings.EMAIL_PASSWORD, - DestinationSettings.ALLOW_LIST + DestinationSettings.ALLOW_LIST, + AWSSettings.SNS_IAM_USER_ACCESS_KEY, + AWSSettings.SNS_IAM_USER_SECRET_KEY ) } diff --git a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/MonitorRunner.kt b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/MonitorRunner.kt index 224e6a1b..289eac33 100644 --- a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/MonitorRunner.kt +++ b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/MonitorRunner.kt @@ -47,6 +47,7 @@ import com.amazon.opendistroforelasticsearch.alerting.model.action.Action.Compan import com.amazon.opendistroforelasticsearch.alerting.model.destination.DestinationContextFactory import com.amazon.opendistroforelasticsearch.alerting.script.TriggerExecutionContext import com.amazon.opendistroforelasticsearch.alerting.script.TriggerScript +import com.amazon.opendistroforelasticsearch.alerting.settings.AWSSettings import com.amazon.opendistroforelasticsearch.alerting.settings.AlertingSettings.Companion.ALERT_BACKOFF_COUNT import com.amazon.opendistroforelasticsearch.alerting.settings.AlertingSettings.Companion.ALERT_BACKOFF_MILLIS import com.amazon.opendistroforelasticsearch.alerting.settings.AlertingSettings.Companion.MOVE_ALERTS_BACKOFF_COUNT @@ -123,6 +124,7 @@ class MonitorRunner( @Volatile private var destinationSettings = loadDestinationSettings(settings) @Volatile private var destinationContextFactory = DestinationContextFactory(client, xContentRegistry, destinationSettings) + @Volatile private var awsSettings = AWSSettings.parse(settings) init { clusterService.clusterSettings.addSettingsUpdateConsumer(ALERT_BACKOFF_MILLIS, ALERT_BACKOFF_COUNT) { @@ -535,6 +537,7 @@ class MonitorRunner( val destinationCtx = destinationContextFactory.getDestinationContext(destination) actionOutput[MESSAGE_ID] = destination.publish( + awsSettings, actionOutput[SUBJECT], actionOutput[MESSAGE]!!, destinationCtx diff --git a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/destination/Destination.kt b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/destination/Destination.kt index dc21da93..3492da6b 100644 --- a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/destination/Destination.kt +++ b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/destination/Destination.kt @@ -20,6 +20,7 @@ import com.amazon.opendistroforelasticsearch.alerting.destination.message.BaseMe import com.amazon.opendistroforelasticsearch.alerting.destination.message.ChimeMessage import com.amazon.opendistroforelasticsearch.alerting.destination.message.CustomWebhookMessage import com.amazon.opendistroforelasticsearch.alerting.destination.message.EmailMessage +import com.amazon.opendistroforelasticsearch.alerting.destination.message.SNSMessage import com.amazon.opendistroforelasticsearch.alerting.destination.message.SlackMessage import com.amazon.opendistroforelasticsearch.alerting.destination.response.DestinationResponse import com.amazon.opendistroforelasticsearch.alerting.elasticapi.convertToMap @@ -27,6 +28,7 @@ import com.amazon.opendistroforelasticsearch.alerting.elasticapi.instant import com.amazon.opendistroforelasticsearch.alerting.elasticapi.optionalTimeField import com.amazon.opendistroforelasticsearch.alerting.elasticapi.optionalUserField import com.amazon.opendistroforelasticsearch.alerting.model.destination.email.Email +import com.amazon.opendistroforelasticsearch.alerting.settings.AWSSettings import com.amazon.opendistroforelasticsearch.alerting.util.DestinationType import com.amazon.opendistroforelasticsearch.alerting.util.IndexUtils.Companion.NO_SCHEMA_VERSION import com.amazon.opendistroforelasticsearch.commons.authuser.User @@ -56,6 +58,7 @@ data class Destination( val lastUpdateTime: Instant, val chime: Chime?, val slack: Slack?, + val sns: SNS?, val customWebhook: CustomWebhook?, val email: Email? ) : ToXContent { @@ -96,6 +99,8 @@ data class Destination( chime?.writeTo(out) out.writeBoolean(slack != null) slack?.writeTo(out) + out.writeBoolean(sns != null) + sns?.writeTo(out) out.writeBoolean(customWebhook != null) customWebhook?.writeTo(out) out.writeBoolean(email != null) @@ -118,6 +123,7 @@ data class Destination( const val LAST_UPDATE_TIME_FIELD = "last_update_time" const val CHIME = "chime" const val SLACK = "slack" + const val SNS_TYPE = "sns" const val CUSTOMWEBHOOK = "custom_webhook" const val EMAIL = "email" @@ -141,6 +147,7 @@ data class Destination( var user: User? = null lateinit var type: String var slack: Slack? = null + var sns: SNS? = null var chime: Chime? = null var customWebhook: CustomWebhook? = null var email: Email? = null @@ -169,6 +176,9 @@ data class Destination( SLACK -> { slack = Slack.parse(xcp) } + SNS_TYPE -> { + sns = SNS.parse(xcp) + } CUSTOMWEBHOOK -> { customWebhook = CustomWebhook.parse(xcp) } @@ -197,6 +207,7 @@ data class Destination( lastUpdateTime ?: Instant.now(), chime, slack, + sns, customWebhook, email) } @@ -229,6 +240,7 @@ data class Destination( lastUpdateTime = sin.readInstant(), chime = Chime.readFrom(sin), slack = Slack.readFrom(sin), + sns = SNS.readFrom(sin), customWebhook = CustomWebhook.readFrom(sin), email = Email.readFrom(sin) ) @@ -236,7 +248,7 @@ data class Destination( } @Throws(IOException::class) - fun publish(compiledSubject: String?, compiledMessage: String, destinationCtx: DestinationContext): String { + fun publish(AWSSettings: AWSSettings, compiledSubject: String?, compiledMessage: String, destinationCtx: DestinationContext): String { val destinationMessage: BaseMessage val responseContent: String val responseStatusCode: Int @@ -255,6 +267,16 @@ data class Destination( .withMessage(messageContent) .build() } + DestinationType.SNS -> { + destinationMessage = SNSMessage.Builder(name) + .withRoleArn(sns?.roleARN) + .withTopicArn(sns?.topicARN) + .withIAMAccessKey(AWSSettings.iamUserAccessKey) + .withIAMSecretKey(AWSSettings.iamUserSecretKey) + .withSubject(compiledSubject) + .withMessage(compiledMessage) + .build() + } DestinationType.CUSTOM_WEBHOOK -> { destinationMessage = CustomWebhookMessage.Builder(name) .withUrl(customWebhook?.url) @@ -298,6 +320,7 @@ data class Destination( when (type) { DestinationType.CHIME -> content = chime?.convertToMap()?.get(type.value) DestinationType.SLACK -> content = slack?.convertToMap()?.get(type.value) + DestinationType.SNS -> content = sns?.convertToMap()?.get(type.value) DestinationType.CUSTOM_WEBHOOK -> content = customWebhook?.convertToMap()?.get(type.value) DestinationType.EMAIL -> content = email?.convertToMap()?.get(type.value) DestinationType.TEST_ACTION -> content = "dummy" diff --git a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/destination/SNS.kt b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/destination/SNS.kt index ad63cb2b..15f27e67 100644 --- a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/destination/SNS.kt +++ b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/destination/SNS.kt @@ -15,6 +15,10 @@ package com.amazon.opendistroforelasticsearch.alerting.model.destination +import com.amazon.opendistroforelasticsearch.alerting.elasticapi.optionalStringField +import com.amazon.opendistroforelasticsearch.alerting.util.DestinationType +import org.elasticsearch.common.io.stream.StreamInput +import org.elasticsearch.common.io.stream.StreamOutput import org.elasticsearch.common.xcontent.ToXContent import org.elasticsearch.common.xcontent.XContentBuilder import org.elasticsearch.common.xcontent.XContentParser @@ -23,24 +27,32 @@ import java.io.IOException import java.lang.IllegalStateException import java.util.regex.Pattern -data class SNS(val topicARN: String, val roleARN: String) : ToXContent { +data class SNS(val topicARN: String, val roleARN: String?) : ToXContent { init { require(SNS_ARN_REGEX.matcher(topicARN).find()) { "Invalid AWS SNS topic ARN: $topicARN" } - require(IAM_ARN_REGEX.matcher(roleARN).find()) { "Invalid AWS role ARN: $roleARN " } + if (roleARN != null) { + require(IAM_ARN_REGEX.matcher(roleARN).find()) { "Invalid AWS role ARN: $roleARN " } + } } override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { return builder.startObject(SNS_TYPE) .field(TOPIC_ARN_FIELD, topicARN) - .field(ROLE_ARN_FIELD, roleARN) + .optionalStringField(ROLE_ARN_FIELD, roleARN) .endObject() } + @Throws(IOException::class) + fun writeTo(out: StreamOutput) { + out.writeString(topicARN) + out.writeOptionalString(roleARN) + } + companion object { private val SNS_ARN_REGEX = Pattern.compile("^arn:aws(-[^:]+)?:sns:([a-zA-Z0-9-]+):([0-9]{12}):([a-zA-Z0-9-_]+)$") - private val IAM_ARN_REGEX = Pattern.compile("^arn:aws(-[^:]+)?:iam::([0-9]{12}):([a-zA-Z0-9-/_]+)$") + private val IAM_ARN_REGEX = Pattern.compile("^arn:aws(-[^:]+)?:iam::([0-9]{12}):([a-zA-Z_0-9+=,.@\\-_/]+)$") const val TOPIC_ARN_FIELD = "topic_arn" const val ROLE_ARN_FIELD = "role_arn" @@ -50,7 +62,7 @@ data class SNS(val topicARN: String, val roleARN: String) : ToXContent { @Throws(IOException::class) fun parse(xcp: XContentParser): SNS { lateinit var topicARN: String - lateinit var roleARN: String + var roleARN: String? = null XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp) while (xcp.nextToken() != XContentParser.Token.END_OBJECT) { @@ -64,8 +76,21 @@ data class SNS(val topicARN: String, val roleARN: String) : ToXContent { } } } - return SNS(requireNotNull(topicARN) { "SNS Action topic_arn is null" }, - requireNotNull(roleARN) { "SNS Action role_arn is null" }) + if (DestinationType.snsUseIamRole) { + requireNotNull(roleARN) { "SNS Action role_arn is null" } + } + return SNS(requireNotNull(topicARN) { "SNS Action topic_arn is null" }, roleARN) + } + + @JvmStatic + @Throws(IOException::class) + fun readFrom(sin: StreamInput): SNS? { + return if (sin.readBoolean()) { + SNS( + topicARN = sin.readString(), + roleARN = sin.readOptionalString() + ) + } else null } } } diff --git a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/settings/AWSSettings.kt b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/settings/AWSSettings.kt new file mode 100644 index 00000000..65fe3d35 --- /dev/null +++ b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/settings/AWSSettings.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.amazon.opendistroforelasticsearch.alerting.settings + +import com.amazon.opendistroforelasticsearch.alerting.util.DestinationType +import org.elasticsearch.common.settings.SecureSetting +import org.elasticsearch.common.settings.SecureString +import org.elasticsearch.common.settings.Settings +import java.io.IOException + +data class AWSSettings( + val iamUserAccessKey: SecureString, + val iamUserSecretKey: SecureString +) { + companion object { + val SNS_IAM_USER_ACCESS_KEY = SecureSetting.secureString( + "opendistro.alerting.destination.sns.access.key", + null + ) + + val SNS_IAM_USER_SECRET_KEY = SecureSetting.secureString( + "opendistro.alerting.destination.sns.secret.key", + null + ) + + @JvmStatic + @Throws(IOException::class) + fun parse(settings: Settings): AWSSettings { + if (SNS_IAM_USER_ACCESS_KEY.get(settings) == null) { + DestinationType.snsUseIamRole = true + } + return AWSSettings( + SNS_IAM_USER_ACCESS_KEY.get(settings), + SNS_IAM_USER_SECRET_KEY.get(settings) + ) + } + } +} diff --git a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/util/DestinationType.kt b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/util/DestinationType.kt index 28672975..38b663ec 100644 --- a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/util/DestinationType.kt +++ b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/util/DestinationType.kt @@ -18,6 +18,7 @@ package com.amazon.opendistroforelasticsearch.alerting.util enum class DestinationType(val value: String) { CHIME("chime"), SLACK("slack"), + SNS("sns"), CUSTOM_WEBHOOK("custom_webhook"), EMAIL("email"), TEST_ACTION("test_action"); @@ -25,4 +26,8 @@ enum class DestinationType(val value: String) { override fun toString(): String { return value } + + companion object { + var snsUseIamRole = false + } } diff --git a/alerting/src/main/plugin-metadata/plugin-security.policy b/alerting/src/main/plugin-metadata/plugin-security.policy index bcee5e9e..c45a5f67 100644 --- a/alerting/src/main/plugin-metadata/plugin-security.policy +++ b/alerting/src/main/plugin-metadata/plugin-security.policy @@ -5,4 +5,28 @@ grant { permission java.net.SocketPermission "*", "connect,resolve"; permission java.net.NetPermission "getProxySelector"; + + // needed because of problems in ClientConfiguration + // TODO: get these fixed in aws sdk + permission java.lang.RuntimePermission "accessDeclaredMembers"; + permission java.lang.RuntimePermission "getClassLoader"; + permission java.net.SocketPermission "*", "connect"; + // Needed because of problems in AmazonSNS: + // When no region is set on a STSClient instance, the + // AWS SDK loads all known partitions from a JSON file and + // uses a Jackson's ObjectMapper for that: this one, in + // version 2.5.3 with the default binding options, tries + // to suppress access checks of ctor/field/method and thus + // requires this special permission. AWS must be fixed to + // uses Jackson correctly and have the correct modifiers + // on binded classes. + // TODO: get these fixed in aws sdk + // See https://github.com/aws/aws-sdk-java/issues/766 + permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; + + // Below is specific for notification SNS client + permission javax.management.MBeanServerPermission "createMBeanServer"; + permission javax.management.MBeanServerPermission "findMBeanServer"; + permission javax.management.MBeanPermission "com.amazonaws.metrics.*", "*"; + permission javax.management.MBeanTrustPermission "register"; }; diff --git a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/AlertingRestTestCase.kt b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/AlertingRestTestCase.kt index aedf40f4..1b6dea05 100644 --- a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/AlertingRestTestCase.kt +++ b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/AlertingRestTestCase.kt @@ -302,6 +302,7 @@ abstract class AlertingRestTestCase : ODFERestTestCase() { lastUpdateTime = Instant.now(), chime = null, slack = null, + sns = null, customWebhook = null, email = null) } 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 0223cb98..d53b520e 100644 --- a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/MonitorRunnerIT.kt +++ b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/MonitorRunnerIT.kt @@ -639,6 +639,7 @@ class MonitorRunnerIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = null, slack = null, + sns = null, customWebhook = null, email = email )) diff --git a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/action/GetDestinationsResponseTests.kt b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/action/GetDestinationsResponseTests.kt index c60703af..5b7bb9b5 100644 --- a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/action/GetDestinationsResponseTests.kt +++ b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/action/GetDestinationsResponseTests.kt @@ -55,6 +55,7 @@ class GetDestinationsResponseTests : ESTestCase() { null, slack, null, + null, null) val req = GetDestinationsResponse(RestStatus.OK, 1, listOf(destination)) diff --git a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/action/IndexDestinationRequestTests.kt b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/action/IndexDestinationRequestTests.kt index 15e1fa46..526525f6 100644 --- a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/action/IndexDestinationRequestTests.kt +++ b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/action/IndexDestinationRequestTests.kt @@ -49,6 +49,7 @@ class IndexDestinationRequestTests : ESTestCase() { Chime("test.com"), null, null, + null, null ) ) @@ -88,6 +89,7 @@ class IndexDestinationRequestTests : ESTestCase() { Chime("test.com"), null, null, + null, null ) ) diff --git a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/action/IndexDestinationResponseTests.kt b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/action/IndexDestinationResponseTests.kt index abb948bc..04a8cdae 100644 --- a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/action/IndexDestinationResponseTests.kt +++ b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/action/IndexDestinationResponseTests.kt @@ -31,7 +31,7 @@ class IndexDestinationResponseTests : ESTestCase() { val req = IndexDestinationResponse("1234", 0L, 1L, 2L, RestStatus.CREATED, Destination("1234", 0L, 1, 1, 1, DestinationType.CHIME, "TestChimeDest", - randomUser(), Instant.now(), Chime("test.com"), null, null, null)) + randomUser(), Instant.now(), Chime("test.com"), null, null, null, null)) assertNotNull(req) val out = BytesStreamOutput() diff --git a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/DestinationTests.kt b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/DestinationTests.kt index a14ba1dc..a9816364 100644 --- a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/DestinationTests.kt +++ b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/DestinationTests.kt @@ -18,6 +18,7 @@ package com.amazon.opendistroforelasticsearch.alerting.model import com.amazon.opendistroforelasticsearch.alerting.model.destination.Chime import com.amazon.opendistroforelasticsearch.alerting.model.destination.CustomWebhook import com.amazon.opendistroforelasticsearch.alerting.model.destination.Destination +import com.amazon.opendistroforelasticsearch.alerting.model.destination.SNS import com.amazon.opendistroforelasticsearch.alerting.model.destination.email.Email import com.amazon.opendistroforelasticsearch.alerting.model.destination.Slack import com.amazon.opendistroforelasticsearch.alerting.parser @@ -114,7 +115,7 @@ class DestinationTests : ESTestCase() { fun `test chime destination create using stream`() { val chimeDest = Destination("1234", 0L, 1, 1, 1, DestinationType.CHIME, "TestChimeDest", - randomUser(), Instant.now(), Chime("test.com"), null, null, null) + randomUser(), Instant.now(), Chime("test.com"), null, null, null, null) val out = BytesStreamOutput() chimeDest.writeTo(out) @@ -130,13 +131,14 @@ class DestinationTests : ESTestCase() { assertNotNull(newDest.lastUpdateTime) assertNotNull(newDest.chime) assertNull(newDest.slack) + assertNull(newDest.sns) assertNull(newDest.customWebhook) assertNull(newDest.email) } fun `test slack destination create using stream`() { val slackDest = Destination("2345", 1L, 2, 1, 1, DestinationType.SLACK, "TestSlackDest", - randomUser(), Instant.now(), null, Slack("mytest.com"), null, null) + randomUser(), Instant.now(), null, Slack("mytest.com"), null, null, null) val out = BytesStreamOutput() slackDest.writeTo(out) @@ -152,6 +154,31 @@ class DestinationTests : ESTestCase() { assertNotNull(newDest.lastUpdateTime) assertNull(newDest.chime) assertNotNull(newDest.slack) + assertNull(newDest.sns) + assertNull(newDest.customWebhook) + assertNull(newDest.email) + } + + fun `test sns destination create using stream`() { + val sns = SNS("arn:aws:sns:us-west-2:475347751589:test-notification", null) + val snsDest = Destination("2345", 1L, 2, 1, 1, DestinationType.SNS, "TestSnsDest", + randomUser(), Instant.now(), null, null, sns, null, null) + + val out = BytesStreamOutput() + snsDest.writeTo(out) + val sin = StreamInput.wrap(out.bytes().toBytesRef().bytes) + val newDest = Destination.readFrom(sin) + + assertNotNull(newDest) + assertEquals("2345", newDest.id) + assertEquals(1, newDest.version) + assertEquals(2, newDest.schemaVersion) + assertEquals(DestinationType.SNS, newDest.type) + assertEquals("TestSnsDest", newDest.name) + assertNotNull(newDest.lastUpdateTime) + assertNull(newDest.chime) + assertNull(newDest.slack) + assertNotNull(newDest.sns) assertNull(newDest.customWebhook) assertNull(newDest.email) } @@ -169,6 +196,7 @@ class DestinationTests : ESTestCase() { Instant.now(), null, null, + null, CustomWebhook( "test.com", "schema", @@ -197,6 +225,7 @@ class DestinationTests : ESTestCase() { assertNotNull(newDest.lastUpdateTime) assertNull(newDest.chime) assertNull(newDest.slack) + assertNull(newDest.sns) assertNotNull(newDest.customWebhook) assertNull(newDest.email) } @@ -214,6 +243,7 @@ class DestinationTests : ESTestCase() { Instant.now(), null, null, + null, CustomWebhook( "test.com", null, @@ -242,6 +272,7 @@ class DestinationTests : ESTestCase() { assertNotNull(newDest.lastUpdateTime) assertNull(newDest.chime) assertNull(newDest.slack) + assertNull(newDest.sns) assertNotNull(newDest.customWebhook) assertNull(newDest.email) } @@ -267,6 +298,7 @@ class DestinationTests : ESTestCase() { null, null, null, + null, Email("3456", recipients) ) @@ -284,6 +316,7 @@ class DestinationTests : ESTestCase() { assertNotNull(newDest.lastUpdateTime) assertNull(newDest.chime) assertNull(newDest.slack) + assertNull(newDest.sns) assertNull(newDest.customWebhook) assertNotNull(newDest.email) @@ -312,4 +345,34 @@ class DestinationTests : ESTestCase() { val parsedDest = Destination.parse(parser(userString)) assertNull(parsedDest.user) } + + fun `test sns destination`() { + DestinationType.snsUseIamRole = true + val sns = SNS("arn:aws:sns:us-west-2:475347751589:test-notification", "arn:aws:iam::853806060000:role/domain/abc") + assertEquals("topic arn is manipulated", sns.topicARN, "arn:aws:sns:us-west-2:475347751589:test-notification") + assertEquals("role arn is manipulated", sns.roleARN, "arn:aws:iam::853806060000:role/domain/abc") + } + + fun `test sns destination without role support`() { + val sns = SNS("arn:aws:sns:us-west-2:475347751589:test-notification", null) + assertEquals("topic arn is manipulated", sns.topicARN, "arn:aws:sns:us-west-2:475347751589:test-notification") + assertNull("role arn is manipulated", sns.roleARN) + } + + fun `test sns destination with invalid topic arn`() { + try { + SNS("arn:asdas:sns:475347751589:test-notification", null) + fail("Creating a sns destination with invalid topic arn did not fail.") + } catch (ignored: IllegalArgumentException) { + } + } + + fun `test sns destination with invalid role arn`() { + try { + DestinationType.snsUseIamRole = true + SNS("arn:aws:sns:us-west-2:475347751589:test-notification", "arn:aws:iamiam::853806060000:role/domain/abc") + fail("Creating a sns destination with invalid role arn did not fail.") + } catch (ignored: IllegalArgumentException) { + } + } } diff --git a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/resthandler/DestinationRestApiIT.kt b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/resthandler/DestinationRestApiIT.kt index e5ea7f0b..eb6ab967 100644 --- a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/resthandler/DestinationRestApiIT.kt +++ b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/resthandler/DestinationRestApiIT.kt @@ -46,6 +46,7 @@ class DestinationRestApiIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = chime, slack = null, + sns = null, customWebhook = null, email = null) val createdDestination = createDestination(destination = destination) @@ -78,6 +79,7 @@ class DestinationRestApiIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = null, slack = slack, + sns = null, customWebhook = null, email = null) val createdDestination = createDestination(destination = destination) @@ -110,6 +112,7 @@ class DestinationRestApiIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = null, slack = null, + sns = null, customWebhook = customWebhook, email = null) val createdDestination = createDestination(destination = destination) @@ -128,6 +131,7 @@ class DestinationRestApiIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = null, slack = null, + sns = null, customWebhook = customWebhook, email = null) val createdDestination = createDestination(destination = destination) @@ -175,6 +179,7 @@ class DestinationRestApiIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = null, slack = null, + sns = null, customWebhook = null, email = email) @@ -223,6 +228,7 @@ class DestinationRestApiIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = chime, slack = null, + sns = null, customWebhook = null, email = null) @@ -253,6 +259,7 @@ class DestinationRestApiIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = null, slack = slack, + sns = null, customWebhook = null, email = null) createDestination(destination = destination) @@ -287,6 +294,7 @@ class DestinationRestApiIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = null, slack = slack, + sns = null, customWebhook = null, email = null) @@ -318,6 +326,7 @@ class DestinationRestApiIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = null, slack = slack, + sns = null, customWebhook = null, email = null) diff --git a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/resthandler/SNSRestApiIT.kt b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/resthandler/SNSRestApiIT.kt new file mode 100644 index 00000000..6f705228 --- /dev/null +++ b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/resthandler/SNSRestApiIT.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.amazon.opendistroforelasticsearch.alerting.resthandler + +import com.amazon.opendistroforelasticsearch.alerting.AlertingRestTestCase +import com.amazon.opendistroforelasticsearch.alerting.model.destination.Destination +import com.amazon.opendistroforelasticsearch.alerting.model.destination.SNS +import com.amazon.opendistroforelasticsearch.alerting.randomUser +import com.amazon.opendistroforelasticsearch.alerting.util.DestinationType +import org.elasticsearch.test.junit.annotations.TestLogging +import org.junit.Assert +import java.time.Instant + +@TestLogging("level:DEBUG", reason = "Debug for tests.") +@Suppress("UNCHECKED_CAST") +class SNSRestApiIT : AlertingRestTestCase() { + + fun `test creating a sns destination`() { + val sns = SNS("arn:aws:sns:us-west-2:475347751589:test-notification", "arn:aws:iam::853806060000:role/domain/abc") + val destination = Destination( + type = DestinationType.SNS, + name = "test", + user = randomUser(), + lastUpdateTime = Instant.now(), + chime = null, + slack = null, + sns = sns, + customWebhook = null, + email = null) + val createdDestination = createDestination(destination = destination) + assertEquals("Incorrect destination name", createdDestination.name, "test") + assertEquals("Incorrect destination type", createdDestination.type, DestinationType.SNS) + Assert.assertNotNull("sns object should not be null", createdDestination.sns) + } + + fun `test updating a sns destination`() { + val destination = createDestination() + val sns = SNS("arn:aws:sns:us-west-2:475347751589:test-notification", "arn:aws:iam::853806060000:role/domain/abc") + var updatedDestination = updateDestination( + destination.copy(name = "updatedName", sns = sns, type = DestinationType.SNS)) + assertEquals("Incorrect destination name after update", updatedDestination.name, "updatedName") + assertEquals("Incorrect destination ID after update", updatedDestination.id, destination.id) + assertEquals("Incorrect destination type after update", updatedDestination.type, DestinationType.SNS) + assertEquals("Incorrect destination sns topic arn after update", + "arn:aws:sns:us-west-2:475347751589:test-notification", updatedDestination.sns?.topicARN) + val updatedSns = SNS("arn:aws:sns:us-west-3:475347751589:test-notification", "arn:aws:iam::123456789012:role/domain/abc") + updatedDestination = updateDestination( + destination.copy(name = "updatedName", sns = updatedSns, type = DestinationType.SNS)) + assertEquals("Incorrect destination sns topic arn after update", + "arn:aws:sns:us-west-3:475347751589:test-notification", updatedDestination.sns?.topicARN) + } +} diff --git a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/resthandler/SecureDestinationRestApiIT.kt b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/resthandler/SecureDestinationRestApiIT.kt index 78ef1bcf..929c8d67 100644 --- a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/resthandler/SecureDestinationRestApiIT.kt +++ b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/resthandler/SecureDestinationRestApiIT.kt @@ -43,6 +43,7 @@ class SecureDestinationRestApiIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = chime, slack = null, + sns = null, customWebhook = null, email = null) val createdDestination = createDestination(destination = destination) @@ -60,6 +61,7 @@ class SecureDestinationRestApiIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = chime, slack = null, + sns = null, customWebhook = null, email = null) @@ -97,6 +99,7 @@ class SecureDestinationRestApiIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = chime, slack = null, + sns = null, customWebhook = null, email = null) val createdDestination = createDestination(destination = destination) @@ -128,6 +131,7 @@ class SecureDestinationRestApiIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = chime, slack = null, + sns = null, customWebhook = null, email = null) @@ -156,6 +160,7 @@ class SecureDestinationRestApiIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = chime, slack = null, + sns = null, customWebhook = null, email = null) val createdDestination = createDestination(destination = destination) @@ -190,6 +195,7 @@ class SecureDestinationRestApiIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = chime, slack = null, + sns = null, customWebhook = null, email = null) @@ -220,6 +226,7 @@ class SecureDestinationRestApiIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = null, slack = slack, + sns = null, customWebhook = null, email = null) @@ -250,6 +257,7 @@ class SecureDestinationRestApiIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = null, slack = slack, + sns = null, customWebhook = null, email = null) diff --git a/build.gradle b/build.gradle index f5c350ba..c82df893 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ buildscript { ext { es_version = '7.10.2' kotlin_version = '1.3.72' + aws_version = "1.11.849" } repositories { diff --git a/core/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/elasticapi/ElasticExtensions.kt b/core/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/elasticapi/ElasticExtensions.kt index fe9d8afe..e0087143 100644 --- a/core/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/elasticapi/ElasticExtensions.kt +++ b/core/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/elasticapi/ElasticExtensions.kt @@ -145,6 +145,13 @@ fun XContentBuilder.optionalUserField(name: String, user: User?): XContentBuilde return this.field(name, user) } +fun XContentBuilder.optionalStringField(name: String, content: String?): XContentBuilder { + if (content == null) { + return nullField(name) + } + return this.field(name, content) +} + fun addFilter(user: User, searchSourceBuilder: SearchSourceBuilder, fieldName: String) { val filterBackendRoles = QueryBuilders.termsQuery(fieldName, user.backendRoles) val queryBuilder = searchSourceBuilder.query() as BoolQueryBuilder diff --git a/notification/build.gradle b/notification/build.gradle index c7ac324a..e9a0c77e 100644 --- a/notification/build.gradle +++ b/notification/build.gradle @@ -21,8 +21,16 @@ apply plugin: 'signing' dependencies { compileOnly "org.elasticsearch:elasticsearch:${es_version}" - compile "org.apache.httpcomponents:httpcore:4.4.5" - compile "org.apache.httpcomponents:httpclient:4.5.10" + compile group: 'commons-net', name: 'commons-net', version: '3.6' + compile "com.amazonaws:aws-java-sdk-core:${aws_version}" + compile "com.amazonaws:aws-java-sdk-sns:${aws_version}" + compile "com.amazonaws:aws-java-sdk-sts:${aws_version}" + compile("org.apache.httpcomponents:httpclient:4.5.10") { + force = true + } + compile("org.apache.httpcomponents:httpcore:4.4.5") { + force = true + } compile "com.sun.mail:javax.mail:1.6.2" testImplementation "org.elasticsearch.test:framework:${es_version}" diff --git a/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/factory/DestinationFactoryProvider.java b/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/factory/DestinationFactoryProvider.java index ee0cfc43..37da847d 100644 --- a/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/factory/DestinationFactoryProvider.java +++ b/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/factory/DestinationFactoryProvider.java @@ -34,6 +34,7 @@ public class DestinationFactoryProvider { destinationFactoryMap.put(DestinationType.SLACK, new SlackDestinationFactory()); destinationFactoryMap.put(DestinationType.CUSTOMWEBHOOK, new CustomWebhookDestinationFactory()); destinationFactoryMap.put(DestinationType.EMAIL, new EmailDestinationFactory()); + destinationFactoryMap.put(DestinationType.SNS, new SNSDestinationFactory()); } /** diff --git a/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/factory/SNSDestinationFactory.java b/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/factory/SNSDestinationFactory.java new file mode 100644 index 00000000..c68fa6b6 --- /dev/null +++ b/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/factory/SNSDestinationFactory.java @@ -0,0 +1,90 @@ +package com.amazon.opendistroforelasticsearch.alerting.destination.factory; + +import com.amazon.opendistroforelasticsearch.alerting.destination.message.SNSMessage; +import com.amazon.opendistroforelasticsearch.alerting.destination.response.DestinationResponse; +import com.amazon.opendistroforelasticsearch.alerting.destination.util.Util; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.auth.STSAssumeRoleSessionCredentialsProvider; +import com.amazonaws.services.sns.AmazonSNS; +import com.amazonaws.services.sns.AmazonSNSClientBuilder; +import com.amazonaws.services.sns.model.PublishResult; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.lucene.queryparser.ext.Extensions.Pair; +import org.elasticsearch.common.Strings; + +import java.util.HashMap; +import java.util.Map; + +public final class SNSDestinationFactory implements DestinationFactory { + private static final Logger logger = LogManager.getLogger(SNSDestinationFactory.class); + + private static final Map, AmazonSNS> snsClientMap = new HashMap<>(); + + /** + * Publishes SNS message + * + * @param message sns message + * @return SNSResponse + */ + @Override + public DestinationResponse publish(SNSMessage message) { + AmazonSNS snsClient = getClient(message); + PublishResult result; + if (!Strings.isNullOrEmpty(message.getSubject())) { + result = snsClient.publish(message.getTopicArn(), message.getMessage(), message.getSubject()); + } else { + result = snsClient.publish(message.getTopicArn(), message.getMessage()); + } + logger.info("Message successfully published: " + result.getMessageId()); + return new DestinationResponse.Builder().withResponseContent(result.getMessageId()) + .withStatusCode(result.getSdkHttpMetadata().getHttpStatusCode()).build(); + } + + /** + * Fetches the client corresponding to a topic role + * + * @param message sns message + * @return AmazonSNS AWS SNS client + */ + @Override + public AmazonSNS getClient(SNSMessage message) { + String credKey; + if (message.getRoleArn() == null) { + String accessKey = message.getIAMAccessKey().toString(); + String secretKey = message.getIAMSecretKey().toString(); + credKey = String.format("%s %s", accessKey, secretKey); + } else { + credKey = message.getRoleArn(); + } + String region = Util.getRegion(message.getTopicArn()); + Pair clientKey = new Pair<>(credKey, region); + if (!snsClientMap.containsKey(clientKey)) { + AmazonSNS snsClient = AmazonSNSClientBuilder.standard() + .withRegion(region) + .withCredentials(getProvider(credKey)).build(); + snsClientMap.put(clientKey, snsClient); + } + + return snsClientMap.get(clientKey); + } + + /** + * Builds the AWSCredentialsProvider + * + * @return AWSCredentialsProvider + * @throws IllegalArgumentException + */ + public AWSCredentialsProvider getProvider(String credKey) throws IllegalArgumentException { + if (credKey.contains(" ")) { + String[] keys = credKey.split(" "); + BasicAWSCredentials awsCredentials = new BasicAWSCredentials(keys[0], keys[1]); + return new AWSStaticCredentialsProvider(awsCredentials); + } else { + return new STSAssumeRoleSessionCredentialsProvider + .Builder(credKey, "es-notification-sns-session").build(); + } + } +} diff --git a/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/message/DestinationType.java b/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/message/DestinationType.java index 4101fb46..79c29137 100644 --- a/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/message/DestinationType.java +++ b/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/message/DestinationType.java @@ -19,5 +19,5 @@ * Supported notification destinations */ public enum DestinationType { - CHIME, SLACK, CUSTOMWEBHOOK, EMAIL + SNS, CHIME, SLACK, CUSTOMWEBHOOK, EMAIL } diff --git a/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/message/SNSMessage.java b/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/message/SNSMessage.java new file mode 100644 index 00000000..f7cc8181 --- /dev/null +++ b/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/message/SNSMessage.java @@ -0,0 +1,163 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.amazon.opendistroforelasticsearch.alerting.destination.message; + +import com.amazon.opendistroforelasticsearch.alerting.destination.util.Util; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.SecureString; + + +/** + * This class holds the content of an SNS message + */ +public class SNSMessage extends BaseMessage { + + private String subject; + private String message; + private String roleArn; + private String topicArn; + private SecureString iamAccessKey; + public SecureString iamSecretKey; + + private SNSMessage(final DestinationType destinationType, final String destinationName, final String roleArn, + final String topicArn, final SecureString iamAccessKey, final SecureString iamSecretKey, + final String subject, final String message) { + + super(destinationType, destinationName, message); + + if (DestinationType.SNS != destinationType) { + throw new IllegalArgumentException("Channel Type does not match SNS"); + } + + if (roleArn == null && (iamAccessKey == null || Strings.isNullOrEmpty(iamAccessKey.toString()))) { + throw new IllegalArgumentException("IAM user access key is missing"); + } + + if (roleArn == null && (iamSecretKey == null || Strings.isNullOrEmpty(iamSecretKey.toString()))) { + throw new IllegalArgumentException("IAM user secret key is missing"); + } + + if (Strings.isNullOrEmpty(topicArn) || !Util.isValidSNSArn(topicArn)) { + throw new IllegalArgumentException("Topic arn is missing/invalid: " + topicArn); + } + + if (roleArn != null && !Util.isValidIAMArn(roleArn)) { + throw new IllegalArgumentException("Role arn is invalid: " + roleArn); + } + + if (Strings.isNullOrEmpty(message)) { + throw new IllegalArgumentException("Message content is missing"); + } + + this.subject = subject; + this.message = message; + this.roleArn = roleArn; + this.topicArn = topicArn; + this.iamAccessKey = iamAccessKey; + this.iamSecretKey = iamSecretKey; + } + + @Override + public String toString() { + + String credentialKey = ", roleArn: " + roleArn; + if (roleArn == null) { + credentialKey = ", AccessKey: " + iamAccessKey; + } + + return "DestinationType: " + destinationType + ", DestinationName:" + destinationName + + credentialKey + ", TopicArn: " + topicArn + ", Subject: " + subject + + ", Message: " + message; + } + + public static class Builder { + private String subject; + private String message; + private String roleArn; + private String topicArn; + private SecureString iamAccessKey; + private SecureString iamSecretKey; + private DestinationType destinationType; + private String destinationName; + + public Builder(String destinationName) { + this.destinationName = destinationName; + this.destinationType = DestinationType.SNS; + } + + public Builder withSubject(String subject) { + this.subject = subject; + return this; + } + + public Builder withMessage(String message) { + this.message = message; + return this; + } + + public Builder withRoleArn(String roleArn) { + this.roleArn = roleArn; + return this; + } + + public Builder withTopicArn(String topicArn) { + this.topicArn = topicArn; + return this; + } + + public Builder withIAMAccessKey(SecureString iamAccessKey) { + this.iamAccessKey = iamAccessKey; + return this; + } + + public Builder withIAMSecretKey(SecureString iamSecretKey) { + this.iamSecretKey = iamSecretKey; + return this; + } + + public SNSMessage build() { + SNSMessage snsMessage = new SNSMessage(this.destinationType, this.destinationName, this.roleArn, this.topicArn, + this.iamAccessKey, this.iamSecretKey, this.subject, this.message); + + return snsMessage; + } + } + + public String getSubject() { + return subject; + } + + public String getMessage() { + return message; + } + + public String getRoleArn() { + return roleArn; + } + + public String getTopicArn() { + return topicArn; + } + + public SecureString getIAMAccessKey() { + return iamAccessKey; + } + + public SecureString getIAMSecretKey() { + return iamSecretKey; + } + +} diff --git a/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/util/Util.java b/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/util/Util.java new file mode 100644 index 00000000..3974bfa1 --- /dev/null +++ b/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/util/Util.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.amazon.opendistroforelasticsearch.alerting.destination.util; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.ValidationException; + +import java.util.regex.Pattern; + +public class Util { + + private Util() {} + + public static final Pattern SNS_ARN_REGEX = Pattern.compile("^arn:aws(-[^:]+)?:sns:([a-zA-Z0-9-]+):([0-9]{12}):([a-zA-Z0-9-_]+)$"); + public static final Pattern IAM_ARN_REGEX = Pattern.compile("^arn:aws(-[^:]+)?:iam::([0-9]{12}):([a-zA-Z0-9-/_]+)$"); + + public static String getRegion(String arn) { + // sample topic arn arn:aws:sns:us-west-2:075315751589:test-notification + if (isValidSNSArn(arn)) { + return arn.split(":")[3]; + } + throw new IllegalArgumentException("Unable to retrieve region from ARN " + arn); + } + + public static boolean isValidIAMArn(String arn) { + return Strings.hasLength(arn) && IAM_ARN_REGEX.matcher(arn).find(); + } + + public static boolean isValidSNSArn(String arn) throws ValidationException { + return Strings.hasLength(arn) && SNS_ARN_REGEX.matcher(arn).find(); + } +} diff --git a/notification/src/test/java/com/amazon/opendistroforelasticsearch/alerting/destination/SNSDestinationTest.java b/notification/src/test/java/com/amazon/opendistroforelasticsearch/alerting/destination/SNSDestinationTest.java new file mode 100644 index 00000000..fbedb5e6 --- /dev/null +++ b/notification/src/test/java/com/amazon/opendistroforelasticsearch/alerting/destination/SNSDestinationTest.java @@ -0,0 +1,156 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.amazon.opendistroforelasticsearch.alerting.destination; + +import com.amazon.opendistroforelasticsearch.alerting.destination.factory.SNSDestinationFactory; +import com.amazon.opendistroforelasticsearch.alerting.destination.message.SNSMessage; +import com.amazonaws.services.sns.AmazonSNS; +import org.elasticsearch.common.settings.SecureString; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class SNSDestinationTest { + + @Test(expected = IllegalArgumentException.class) + public void testCreateTopicArnMissingMessage() { + try { + SNSMessage message = new SNSMessage.Builder("sms").withMessage("dummyMessage") + .withRoleArn("arn:aws:iam::853806060000:role/domain/abc").build(); + } catch (Exception ex) { + assertEquals("Topic arn is missing/invalid: null", ex.getMessage()); + throw ex; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateIamAccessKeyMissingMessage() { + try { + SNSMessage message = new SNSMessage.Builder("sms").withMessage("dummyMessage") + .withIAMSecretKey(new SecureString("randomSecretString")) + .withTopicArn("arn:aws:sns:us-west-2:475313751589:test-notification").build(); + } catch (Exception ex) { + assertEquals("IAM user access key is missing", ex.getMessage()); + throw ex; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateIamSecretKeyMissingMessage() { + try { + SNSMessage message = new SNSMessage.Builder("sms").withMessage("dummyMessage") + .withIAMAccessKey(new SecureString("randomAccessString")) + .withTopicArn("arn:aws:sns:us-west-2:475313751589:test-notification").build(); + } catch (Exception ex) { + assertEquals("IAM user secret key is missing", ex.getMessage()); + throw ex; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateContentMissingMessage() { + try { + SNSMessage message = new SNSMessage.Builder("sms") + .withRoleArn("arn:aws:iam::853806060000:role/domain/abc") + .withTopicArn("arn:aws:sns:us-west-2:475313751589:test-notification").build(); + } catch (Exception ex) { + assertEquals("Message content is missing", ex.getMessage()); + throw ex; + } + } + + @Test(expected = IllegalArgumentException.class) + public void testInValidRoleMessage() { + try { + SNSMessage message = new SNSMessage.Builder("sms").withMessage("dummyMessage") + .withRoleArn("dummyRole") + .withTopicArn("arn:aws:sns:us-west-2:475313751589:test-notification").build(); + } catch (Exception ex) { + assertEquals("Role arn is invalid: dummyRole", ex.getMessage()); + throw ex; + } + } + + @Test + public void testValidMessage() { + SNSMessage message = new SNSMessage.Builder("sms").withMessage("dummyMessage") + .withSubject("dummySubject") + .withRoleArn("arn:aws:iam::853806060000:role/domain/abc") + .withTopicArn("arn:aws:sns:us-west-2:475313751589:test-notification").build(); + assertEquals("sms", message.getChannelName()); + } + + @Test + public void testToStringWithRole() { + SNSMessage message = new SNSMessage.Builder("sms").withMessage("dummyMessage") + .withSubject("dummySubject") + .withRoleArn("arn:aws:iam::853806060000:role/domain/abc") + .withTopicArn("arn:aws:sns:us-west-2:475313751589:test-notification").build(); + assertEquals("DestinationType: SNS, DestinationName:sms, " + + "roleArn: arn:aws:iam::853806060000:role/domain/abc, " + + "TopicArn: arn:aws:sns:us-west-2:475313751589:test-notification, " + + "Subject: dummySubject, Message: dummyMessage", message.toString()); + } + + @Test + public void testToStringWithoutRole() { + SNSMessage message = new SNSMessage.Builder("sms").withMessage("dummyMessage") + .withSubject("dummySubject") + .withIAMAccessKey(new SecureString("randomAccessString")) + .withIAMSecretKey(new SecureString("randomSecretString")) + .withTopicArn("arn:aws:sns:us-west-2:475313751589:test-notification").build(); + assertEquals("DestinationType: SNS, DestinationName:sms, AccessKey: randomAccessString, " + + "TopicArn: arn:aws:sns:us-west-2:475313751589:test-notification, Subject: dummySubject, " + + "Message: dummyMessage", message.toString()); + } + + @Test(expected = IllegalArgumentException.class) + public void testInValidChannelName() { + try { + SNSMessage message = new SNSMessage.Builder("").withMessage("dummyMessage") + .withRoleArn("arn:aws:iam::853806060000:role/domain/abc") + .withTopicArn("arn:aws:sns:us-west-2:475313751589:test-notification").build(); + } catch (Exception ex) { + assertEquals("Channel name must be defined", ex.getMessage()); + throw ex; + } + } + + @Test + public void testGetClientWithRole() { + SNSMessage message = new SNSMessage.Builder("sms").withMessage("dummyMessage") + .withSubject("dummySubject") + .withRoleArn("arn:aws:iam::853806060000:role/domain/abc") + .withTopicArn("arn:aws:sns:us-west-2:475313751589:test-notification").build(); + SNSDestinationFactory snsDestinationFactory = new SNSDestinationFactory(); + AmazonSNS sns = snsDestinationFactory.getClient(message); + assertNotNull(sns); + } + + @Test + public void testGetClientWithoutRole() { + SNSMessage message = new SNSMessage.Builder("sms").withMessage("dummyMessage") + .withSubject("dummySubject") + .withIAMAccessKey(new SecureString("randomAccessString")) + .withIAMSecretKey(new SecureString("randomSecretString")) + .withTopicArn("arn:aws:sns:us-west-2:475313751589:test-notification").build(); + SNSDestinationFactory snsDestinationFactory = new SNSDestinationFactory(); + AmazonSNS sns = snsDestinationFactory.getClient(message); + assertNotNull(sns); + } +} + diff --git a/notification/src/test/java/com/amazon/opendistroforelasticsearch/alerting/destination/UtilTest.java b/notification/src/test/java/com/amazon/opendistroforelasticsearch/alerting/destination/UtilTest.java new file mode 100644 index 00000000..4811eba0 --- /dev/null +++ b/notification/src/test/java/com/amazon/opendistroforelasticsearch/alerting/destination/UtilTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.amazon.opendistroforelasticsearch.alerting.destination; + +import com.amazon.opendistroforelasticsearch.alerting.destination.util.Util; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class UtilTest { + + @Test + public void testValidSNSTopicArn() { + String topicArn = "arn:aws:sns:us-west-2:475313751589:test-notification"; + assertTrue("topic arn should be valid", Util.isValidSNSArn(topicArn)); + topicArn = "arn:aws-cn:sns:us-west-2:475313751589:test-notification"; + assertTrue("topic arn should be valid", Util.isValidSNSArn(topicArn)); + } + + @Test + public void testInvalidSNSTopicArn() { + String topicArn = "arn:aws:sns1:us-west-2:475313751589:test-notification"; + assertFalse("topic arn should be Invalid", Util.isValidSNSArn(topicArn)); + } + + @Test + public void testIAMRoleArn() { + String roleArn = "arn:aws:iam::853806060000:role/domain/abc"; + assertTrue("IAM role arn should be valid", Util.isValidIAMArn(roleArn)); + } + + @Test + public void testInvalidIAMRoleArn() { + String roleArn = "arn:aws:iam::85380606000000000:role/domain/010-asdf"; + assertFalse("IAM role arn should be Invalid", Util.isValidIAMArn(roleArn)); + } + + @Test + public void testGetRegion() { + String topicArn = "arn:aws:sns:us-west-2:475313751589:test-notification"; + assertEquals(Util.getRegion(topicArn), "us-west-2"); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidGetRegion() { + String topicArn = "arn:aws:abs:us-west-2:475313751589:test-notification"; + assertEquals(Util.getRegion(topicArn), "us-west-2"); + } +} From 3ade7cce8ec5e38eb2d16924490604176d05f71c Mon Sep 17 00:00:00 2001 From: Ashish Agrawal Date: Tue, 9 Feb 2021 16:47:46 -0800 Subject: [PATCH 2/6] add support for periods --- .../alerting/destination/util/Util.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/util/Util.java b/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/util/Util.java index 3974bfa1..a0322d72 100644 --- a/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/util/Util.java +++ b/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/util/Util.java @@ -24,8 +24,8 @@ public class Util { private Util() {} - public static final Pattern SNS_ARN_REGEX = Pattern.compile("^arn:aws(-[^:]+)?:sns:([a-zA-Z0-9-]+):([0-9]{12}):([a-zA-Z0-9-_]+)$"); - public static final Pattern IAM_ARN_REGEX = Pattern.compile("^arn:aws(-[^:]+)?:iam::([0-9]{12}):([a-zA-Z0-9-/_]+)$"); + public static final Pattern SNS_ARN_REGEX = Pattern.compile("^arn:aws(-[^:]+)?:sns:([a-zA-Z0-9-]+):([0-9]{12}):([a-zA-Z_0-9+=,.@\\-_/]+)$"); + public static final Pattern IAM_ARN_REGEX = Pattern.compile("^arn:aws(-[^:]+)?:iam::([0-9]{12}):([a-zA-Z_0-9+=,.@\\-_/]+)$"); public static String getRegion(String arn) { // sample topic arn arn:aws:sns:us-west-2:075315751589:test-notification From ec6cab0902c880ef61e0e9f8dad3faa62956b9e8 Mon Sep 17 00:00:00 2001 From: Ashish Agrawal Date: Wed, 24 Feb 2021 12:11:20 -0800 Subject: [PATCH 3/6] make small fixes --- .../opendistroforelasticsearch/alerting/MonitorRunner.kt | 3 +++ .../alerting/model/destination/Destination.kt | 6 +++--- .../alerting/settings/AWSSettings.kt | 8 +++++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/MonitorRunner.kt b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/MonitorRunner.kt index 289eac33..2f660b55 100644 --- a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/MonitorRunner.kt +++ b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/MonitorRunner.kt @@ -142,6 +142,9 @@ class MonitorRunner( fun reloadDestinationSettings(settings: Settings) { destinationSettings = loadDestinationSettings(settings) + // Update awsSettings for SNS destination type + awsSettings = AWSSettings.parse(settings) + // Update destinationContextFactory as well since destinationSettings has been updated destinationContextFactory.updateDestinationSettings(destinationSettings) } diff --git a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/destination/Destination.kt b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/destination/Destination.kt index 3492da6b..50ccff6a 100644 --- a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/destination/Destination.kt +++ b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/destination/Destination.kt @@ -248,7 +248,7 @@ data class Destination( } @Throws(IOException::class) - fun publish(AWSSettings: AWSSettings, compiledSubject: String?, compiledMessage: String, destinationCtx: DestinationContext): String { + fun publish(awsSettings: AWSSettings, compiledSubject: String?, compiledMessage: String, destinationCtx: DestinationContext): String { val destinationMessage: BaseMessage val responseContent: String val responseStatusCode: Int @@ -271,8 +271,8 @@ data class Destination( destinationMessage = SNSMessage.Builder(name) .withRoleArn(sns?.roleARN) .withTopicArn(sns?.topicARN) - .withIAMAccessKey(AWSSettings.iamUserAccessKey) - .withIAMSecretKey(AWSSettings.iamUserSecretKey) + .withIAMAccessKey(awsSettings.iamUserAccessKey) + .withIAMSecretKey(awsSettings.iamUserSecretKey) .withSubject(compiledSubject) .withMessage(compiledMessage) .build() diff --git a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/settings/AWSSettings.kt b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/settings/AWSSettings.kt index 65fe3d35..a86a5825 100644 --- a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/settings/AWSSettings.kt +++ b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/settings/AWSSettings.kt @@ -21,6 +21,10 @@ import org.elasticsearch.common.settings.SecureString import org.elasticsearch.common.settings.Settings import java.io.IOException +/** + * Settings specific to AWS resources. This class is separated from the other settings classes to store anything specific to + * AWS resources. + */ data class AWSSettings( val iamUserAccessKey: SecureString, val iamUserSecretKey: SecureString @@ -39,9 +43,7 @@ data class AWSSettings( @JvmStatic @Throws(IOException::class) fun parse(settings: Settings): AWSSettings { - if (SNS_IAM_USER_ACCESS_KEY.get(settings) == null) { - DestinationType.snsUseIamRole = true - } + DestinationType.snsUseIamRole = SNS_IAM_USER_ACCESS_KEY.get(settings) == null return AWSSettings( SNS_IAM_USER_ACCESS_KEY.get(settings), SNS_IAM_USER_SECRET_KEY.get(settings) From 1ac39f2049cf00dba56fa9e1ead7b1e5d39f7f95 Mon Sep 17 00:00:00 2001 From: Ashish Agrawal Date: Wed, 17 Mar 2021 11:18:14 -0700 Subject: [PATCH 4/6] add config support for alternative implementations --- .../alerting/model/destination/Destination.kt | 5 +- .../factory/DestinationFactoryProvider.java | 15 ++---- .../destination/util/PropertyHelper.java | 47 +++++++++++++++++++ notification/src/main/resources/di.properties | 5 ++ 4 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/util/PropertyHelper.java create mode 100644 notification/src/main/resources/di.properties diff --git a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/destination/Destination.kt b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/destination/Destination.kt index 83bc0a44..a3d84ed8 100644 --- a/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/destination/Destination.kt +++ b/alerting/src/main/kotlin/com/amazon/opendistroforelasticsearch/alerting/model/destination/Destination.kt @@ -256,7 +256,6 @@ data class Destination( destinationCtx: DestinationContext, denyHostRanges: List ): String { - val destinationMessage: BaseMessage val responseContent: String val responseStatusCode: Int @@ -315,7 +314,9 @@ data class Destination( } } - validateDestinationUri(destinationMessage, denyHostRanges) + if (type !== DestinationType.SNS) { + validateDestinationUri(destinationMessage, denyHostRanges) + } val response = Notification.publish(destinationMessage) as DestinationResponse responseContent = response.responseContent responseStatusCode = response.statusCode diff --git a/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/factory/DestinationFactoryProvider.java b/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/factory/DestinationFactoryProvider.java index 37da847d..38c76479 100644 --- a/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/factory/DestinationFactoryProvider.java +++ b/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/factory/DestinationFactoryProvider.java @@ -16,6 +16,7 @@ package com.amazon.opendistroforelasticsearch.alerting.destination.factory; import com.amazon.opendistroforelasticsearch.alerting.destination.message.DestinationType; +import com.amazon.opendistroforelasticsearch.alerting.destination.util.PropertyHelper; import java.util.HashMap; import java.util.Map; @@ -27,15 +28,7 @@ */ public class DestinationFactoryProvider { - private static Map destinationFactoryMap = new HashMap<>(); - - static { - destinationFactoryMap.put(DestinationType.CHIME, new ChimeDestinationFactory()); - destinationFactoryMap.put(DestinationType.SLACK, new SlackDestinationFactory()); - destinationFactoryMap.put(DestinationType.CUSTOMWEBHOOK, new CustomWebhookDestinationFactory()); - destinationFactoryMap.put(DestinationType.EMAIL, new EmailDestinationFactory()); - destinationFactoryMap.put(DestinationType.SNS, new SNSDestinationFactory()); - } + private static final Map destinationFactoryMap = PropertyHelper.getDestinationFactoryMap(); /** * Fetches the right channel factory based on the type of the channel @@ -45,7 +38,7 @@ public class DestinationFactoryProvider { */ public static DestinationFactory getFactory(DestinationType destinationType) { if (!destinationFactoryMap.containsKey(destinationType)) { - throw new IllegalArgumentException("Invalid channel type"); + throw new IllegalArgumentException("Invalid channel type: " + destinationType.name()); } return destinationFactoryMap.get(destinationType); } @@ -53,7 +46,7 @@ public static DestinationFactory getFactory(DestinationType destinationType) { /* * This function is to mock hooks for the unit test */ - public static void setFactory(DestinationType type, DestinationFactory factory) { + public static void setFactory(DestinationType type, DestinationFactory factory) { destinationFactoryMap.put(type, factory); } } diff --git a/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/util/PropertyHelper.java b/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/util/PropertyHelper.java new file mode 100644 index 00000000..a3a0e37a --- /dev/null +++ b/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/util/PropertyHelper.java @@ -0,0 +1,47 @@ +package com.amazon.opendistroforelasticsearch.alerting.destination.util; + +import com.amazon.opendistroforelasticsearch.alerting.destination.factory.DestinationFactory; +import com.amazon.opendistroforelasticsearch.alerting.destination.message.DestinationType; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.net.URL; +import java.util.*; +import java.util.stream.Collectors; + +public class PropertyHelper { + + private static final Logger logger = LogManager.getLogger(PropertyHelper.class); + + private static Properties properties = new Properties(); + + static { + try { + URL guiceUrl = Class.forName("com.amazon.opendistroforelasticsearch.alerting.destination.util.PropertyHelper") + .getClassLoader().getResource("di.properties"); + properties.load(guiceUrl.openStream()); + } catch (Exception e) { + logger.error("Failed to load properties data"); + } + } + + public static Map getDestinationFactoryMap() { + Map destinationFactoryMap = new HashMap<>(); + String [] destinations = ((String) properties.get("destinations")).split(","); + for (String destination : destinations) { + try { + Class destClass = Class.forName((String) properties.get(destination)); + destinationFactoryMap.put(DestinationType.valueOf(destination), (DestinationFactory) destClass.getDeclaredConstructor().newInstance()); + } catch (Exception e) { + logger.error("Cannot create DestinationFactory, {}", destination, e); + } + } + return destinationFactoryMap; + } + + public static List getBlacklistedIpRanges() { + return Arrays.asList(((String) properties.getOrDefault("ipRanges", "")).split(",")).stream() + .filter(ipString -> !ipString.isBlank()) + .collect(Collectors.toList()); + } +} diff --git a/notification/src/main/resources/di.properties b/notification/src/main/resources/di.properties new file mode 100644 index 00000000..066da5c7 --- /dev/null +++ b/notification/src/main/resources/di.properties @@ -0,0 +1,5 @@ +destinations=CHIME,SLACK,CUSTOMWEBHOOK,SNS +CHIME=com.amazon.opendistroforelasticsearch.alerting.destination.factory.ChimeDestinationFactory +SLACK=com.amazon.opendistroforelasticsearch.alerting.destination.factory.SlackDestinationFactory +CUSTOMWEBHOOK=com.amazon.opendistroforelasticsearch.alerting.destination.factory.CustomWebhookDestinationFactory +SNS=com.amazon.opendistroforelasticsearch.alerting.destination.factory.SNSDestinationFactory From 5c2b6c7e42ea9f33a4ab847e395bfe13dbb32658 Mon Sep 17 00:00:00 2001 From: Ashish Agrawal Date: Wed, 17 Mar 2021 11:48:38 -0700 Subject: [PATCH 5/6] fix email problem --- .../opendistroforelasticsearch/alerting/MonitorRunnerIT.kt | 2 ++ notification/src/main/resources/di.properties | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) 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 999e8e2d..4859b14e 100644 --- a/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/MonitorRunnerIT.kt +++ b/alerting/src/test/kotlin/com/amazon/opendistroforelasticsearch/alerting/MonitorRunnerIT.kt @@ -665,6 +665,7 @@ class MonitorRunnerIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = null, slack = null, + sns = null, customWebhook = customWebhook, email = null )) @@ -691,6 +692,7 @@ class MonitorRunnerIT : AlertingRestTestCase() { lastUpdateTime = Instant.now(), chime = null, slack = null, + sns = null, customWebhook = customWebhook, email = null )) diff --git a/notification/src/main/resources/di.properties b/notification/src/main/resources/di.properties index 066da5c7..006e8e33 100644 --- a/notification/src/main/resources/di.properties +++ b/notification/src/main/resources/di.properties @@ -1,5 +1,6 @@ -destinations=CHIME,SLACK,CUSTOMWEBHOOK,SNS +destinations=CHIME,SLACK,CUSTOMWEBHOOK,EMAIL,SNS CHIME=com.amazon.opendistroforelasticsearch.alerting.destination.factory.ChimeDestinationFactory SLACK=com.amazon.opendistroforelasticsearch.alerting.destination.factory.SlackDestinationFactory CUSTOMWEBHOOK=com.amazon.opendistroforelasticsearch.alerting.destination.factory.CustomWebhookDestinationFactory +EMAIL=com.amazon.opendistroforelasticsearch.alerting.destination.factory.EmailDestinationFactory SNS=com.amazon.opendistroforelasticsearch.alerting.destination.factory.SNSDestinationFactory From 558504acd9a263e2effe01b54e461f0cc654b837 Mon Sep 17 00:00:00 2001 From: Ashish Agrawal Date: Mon, 22 Mar 2021 11:27:13 -0700 Subject: [PATCH 6/6] add test --- .../destination/util/PropertyHelper.java | 6 ----- .../destination/PropertyHelperTest.java | 25 +++++++++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 notification/src/test/java/com/amazon/opendistroforelasticsearch/alerting/destination/PropertyHelperTest.java diff --git a/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/util/PropertyHelper.java b/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/util/PropertyHelper.java index a3a0e37a..741c0af5 100644 --- a/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/util/PropertyHelper.java +++ b/notification/src/main/java/com/amazon/opendistroforelasticsearch/alerting/destination/util/PropertyHelper.java @@ -38,10 +38,4 @@ public static Map getDestinationFactoryMap( } return destinationFactoryMap; } - - public static List getBlacklistedIpRanges() { - return Arrays.asList(((String) properties.getOrDefault("ipRanges", "")).split(",")).stream() - .filter(ipString -> !ipString.isBlank()) - .collect(Collectors.toList()); - } } diff --git a/notification/src/test/java/com/amazon/opendistroforelasticsearch/alerting/destination/PropertyHelperTest.java b/notification/src/test/java/com/amazon/opendistroforelasticsearch/alerting/destination/PropertyHelperTest.java new file mode 100644 index 00000000..f2b9ab51 --- /dev/null +++ b/notification/src/test/java/com/amazon/opendistroforelasticsearch/alerting/destination/PropertyHelperTest.java @@ -0,0 +1,25 @@ +package com.amazon.opendistroforelasticsearch.alerting.destination; + +import com.amazon.opendistroforelasticsearch.alerting.destination.factory.DestinationFactory; +import com.amazon.opendistroforelasticsearch.alerting.destination.message.DestinationType; +import com.amazon.opendistroforelasticsearch.alerting.destination.util.PropertyHelper; +import org.junit.Test; + +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class PropertyHelperTest { + + @Test + public void testDestinationFactoryList() { + Map destMap = PropertyHelper.getDestinationFactoryMap(); + assertEquals(5, destMap.size()); + assertTrue(destMap.containsKey(DestinationType.CHIME)); + assertTrue(destMap.containsKey(DestinationType.SLACK)); + assertTrue(destMap.containsKey(DestinationType.SNS)); + assertTrue(destMap.containsKey(DestinationType.CUSTOMWEBHOOK)); + assertTrue(destMap.containsKey(DestinationType.EMAIL)); + } +}