From add210b439f724200a101b616d3f2f0b653aae19 Mon Sep 17 00:00:00 2001 From: danielkyalo599 Date: Fri, 10 Feb 2023 16:54:51 +0300 Subject: [PATCH 1/3] Added feature support for microsoft teams webhoo Signed-off-by: danielkyalo599 --- .../spi/model/destination/DestinationType.kt | 2 +- .../destination/MicrosoftTeamsDestination.kt | 12 +++ .../spi/utils/ValidationHelpersTests.kt | 6 +- .../transport/DestinationTransportProvider.kt | 3 +- .../core/NotificationCoreImplTests.kt | 3 +- .../MicrosofTeamsDestinationTests.kt | 84 +++++++++++++++++++ .../notifications/metrics/Metrics.kt | 4 + .../send/SendMessageActionHelper.kt | 34 ++++++-- .../send/SendTestMessageRestHandlerIT.kt | 32 +++++++ 9 files changed, 170 insertions(+), 10 deletions(-) create mode 100644 notifications/core-spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/MicrosoftTeamsDestination.kt create mode 100644 notifications/core/src/test/kotlin/org/opensearch/notifications/core/destinations/MicrosofTeamsDestinationTests.kt diff --git a/notifications/core-spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/DestinationType.kt b/notifications/core-spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/DestinationType.kt index f915e174..cd91128b 100644 --- a/notifications/core-spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/DestinationType.kt +++ b/notifications/core-spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/DestinationType.kt @@ -8,5 +8,5 @@ package org.opensearch.notifications.spi.model.destination * Supported notification destinations */ enum class DestinationType { - CHIME, SLACK, CUSTOM_WEBHOOK, SMTP, SES, SNS + CHIME, SLACK, CUSTOM_WEBHOOK, SMTP, SES, SNS, MICROSOFT_TEAMS } diff --git a/notifications/core-spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/MicrosoftTeamsDestination.kt b/notifications/core-spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/MicrosoftTeamsDestination.kt new file mode 100644 index 00000000..54b85511 --- /dev/null +++ b/notifications/core-spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/MicrosoftTeamsDestination.kt @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.notifications.spi.model.destination +/** + * This class holds the contents of a Microsoft Teams destination + */ +class MicrosoftTeamsDestination( + url: String, +) : WebhookDestination(url, DestinationType.MICROSOFT_TEAMS) diff --git a/notifications/core-spi/src/test/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpersTests.kt b/notifications/core-spi/src/test/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpersTests.kt index 0d803730..67a0cc06 100644 --- a/notifications/core-spi/src/test/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpersTests.kt +++ b/notifications/core-spi/src/test/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpersTests.kt @@ -22,6 +22,7 @@ internal class ValidationHelpersTests { private val LOCAL_HOST_EXTENDED = "https://localhost:6060/service" private val WEBHOOK_URL = "https://test-webhook.com:1234/subdirectory?param1=value1¶m2=¶m3=value3" private val CHIME_URL = "https://domain.com/sample_chime_url#1234567890" + private val MICROSOFT_TEAMS_WEBHOOK_URL = "https://8m7xqz.webhook.office.com/webhookb2/b0885113-57f8-4b61-8f3a-bdf3f4ae2831@500d1839-8666-4320-9f55-59d8838ad8db/IncomingWebhook/84637be48f4245c09b82e735b2cd9335/b7e1bf56-6634-422c-abe8-402e6e95fc68" private val hostDenyList = listOf( "127.0.0.0/8", @@ -94,7 +95,10 @@ internal class ValidationHelpersTests { fun `validator identifies webhook url as valid`() { assert(isValidUrl(WEBHOOK_URL)) } - + @Test + fun `validator identifies webhook url as valid`() { + assert(isValidUrl(MICROSOFT_TEAMS_WEBHOOK_URL)) + } @Test fun `validator identifies chime url as valid`() { assert(isValidUrl(CHIME_URL)) diff --git a/notifications/core/src/main/kotlin/org/opensearch/notifications/core/transport/DestinationTransportProvider.kt b/notifications/core/src/main/kotlin/org/opensearch/notifications/core/transport/DestinationTransportProvider.kt index ed682951..b476e297 100644 --- a/notifications/core/src/main/kotlin/org/opensearch/notifications/core/transport/DestinationTransportProvider.kt +++ b/notifications/core/src/main/kotlin/org/opensearch/notifications/core/transport/DestinationTransportProvider.kt @@ -27,7 +27,8 @@ internal object DestinationTransportProvider { DestinationType.CUSTOM_WEBHOOK to webhookDestinationTransport, DestinationType.SMTP to smtpDestinationTransport, DestinationType.SNS to snsDestinationTransport, - DestinationType.SES to sesDestinationTransport + DestinationType.SES to sesDestinationTransport, + DestinationType.MICROSOFT_TEAMS to webhookDestinationTransport ) /** diff --git a/notifications/core/src/test/kotlin/org/opensearch/notifications/core/NotificationCoreImplTests.kt b/notifications/core/src/test/kotlin/org/opensearch/notifications/core/NotificationCoreImplTests.kt index 872840ec..4b5943e3 100644 --- a/notifications/core/src/test/kotlin/org/opensearch/notifications/core/NotificationCoreImplTests.kt +++ b/notifications/core/src/test/kotlin/org/opensearch/notifications/core/NotificationCoreImplTests.kt @@ -19,7 +19,8 @@ class NotificationCoreImplTests { "sns", "ses_account", "smtp_account", - "email_group" + "email_group", + "microsoft_teams" ) private val defaultConfigFeatures = listOf( "alerting", diff --git a/notifications/core/src/test/kotlin/org/opensearch/notifications/core/destinations/MicrosofTeamsDestinationTests.kt b/notifications/core/src/test/kotlin/org/opensearch/notifications/core/destinations/MicrosofTeamsDestinationTests.kt new file mode 100644 index 00000000..205860e3 --- /dev/null +++ b/notifications/core/src/test/kotlin/org/opensearch/notifications/core/destinations/MicrosofTeamsDestinationTests.kt @@ -0,0 +1,84 @@ +internal class MicrosofTeamsDestinationTests { + companion object { + @JvmStatic + fun escapeSequenceToRaw(): Stream = + Stream.of( + Arguments.of("\n", """\n"""), + Arguments.of("\t", """\t"""), + Arguments.of("\b", """\b"""), + Arguments.of("\r", """\r"""), + Arguments.of("\"", """\""""), + ) + } + + @BeforeEach + fun setup() { + // Stubbing isHostInDenylist() so it doesn't attempt to resolve hosts that don't exist in the unit tests + mockkStatic("org.opensearch.notifications.spi.utils.ValidationHelpersKt") + every { org.opensearch.notifications.spi.utils.isHostInDenylist(any(), any()) } returns false + } + + @Test + fun `test teams message null entity response`() { + val mockHttpClient = mockk() + + // The DestinationHttpClient replaces a null entity with "{}". + val expectedWebhookResponse = DestinationMessageResponse(RestStatus.OK.status, "{}") + // TODO replace EasyMock in all UTs with mockk which fits Kotlin better + val httpResponse = mockk() + every { mockHttpClient.execute(any()) } returns httpResponse + + every { httpResponse.code } returns RestStatus.OK.status + every { httpResponse.entity } returns null + + val httpClient = DestinationHttpClient(mockHttpClient) + val webhookDestinationTransport = WebhookDestinationTransport(httpClient) + DestinationTransportProvider.destinationTransportMap = mapOf(DestinationType.TEAMS to webhookDestinationTransport) + + val title = "test Teams" + val messageText = "Message gughjhjlkh Body emoji test: :) :+1: " + + "link test: http://sample.com email test: marymajor@example.com All member call out: " + + "@All All Present member call out: @Present" + val url = "https://8m7xqz.webhook.office.com/webhookb2/b0885113-57f8-4b61-8f3a-bdf3f4ae2831@500d1839-8666-4320-9f55-59d8838ad8db/IncomingWebhook/84637be48f4245c09b82e735b2cd9335/b7e1bf56-6634-422c-abe8-402e6e95fc68" + + val destination = MicrosoftTeamsDestination(url) + val message = MessageContent(title, messageText) + + val actualTeamsResponse: DestinationMessageResponse = NotificationCoreImpl.sendMessage(destination, message, "referenceId") + + assertEquals(expectedWebhookResponse.statusText, actualTeamsResponse.statusText) + assertEquals(expectedWebhookResponse.statusCode, actualTeamsResponse.statusCode) + } + + @Test + fun `test teams message empty entity response`() { + val mockHttpClient: CloseableHttpClient = EasyMock.createMock(CloseableHttpClient::class.java) + val expectedWebhookResponse = DestinationMessageResponse(RestStatus.OK.status, "") + + // TODO replace EasyMock in all UTs with mockk which fits Kotlin better + val httpResponse = mockk() + every { mockHttpClient.execute(any()) } returns httpResponse + + every { httpResponse.code } returns RestStatus.OK.status + every { httpResponse.entity } returns StringEntity(responseContent) + EasyMock.replay(mockHttpClient) + + val httpClient = DestinationHttpClient(mockHttpClient) + val webhookDestinationTransport = WebhookDestinationTransport(httpClient) + DestinationTransportProvider.destinationTransportMap = mapOf(DestinationType.CHIME to webhookDestinationTransport) + + val title = "test microsoft Teams " + val messageText = "{\"Content\":\"Message gughjhjlkh Body emoji test: :) :+1: " + + "link test: http://sample.com email test: marymajor@example.com All member call out: " + + "@All All Present member call out: @Present\"}" + val url = "https://8m7xqz.webhook.office.com/webhookb2/b0885113-57f8-4b61-8f3a-bdf3f4ae2831@500d1839-8666-4320-9f55-59d8838ad8db/IncomingWebhook/84637be48f4245c09b82e735b2cd9335/b7e1bf56-6634-422c-abe8-402e6e95fc68" + + val destination = MicrosoftTeamsDestination(url) + val message = MessageContent(title, messageText) + + val actualChimeResponse: DestinationMessageResponse = NotificationCoreImpl.sendMessage(destination, message, "referenceId") + + assertEquals(expectedWebhookResponse.statusText, actualChimeResponse.statusText) + assertEquals(expectedWebhookResponse.statusCode, actualChimeResponse.statusCode) + } +} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/Metrics.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/Metrics.kt index 6d610974..f5cda58b 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/Metrics.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/Metrics.kt @@ -179,6 +179,10 @@ enum class Metrics(val metricName: String, val counter: Counter<*>) { "notifications.message_destination.email", BasicCounter() ), + NOTIFICATIONS_MESSAGE_DESTINATION_MICROSOFT_TEAMS( + "notifications.message_destination.microsofTeams", + BasicCounter() + ), NOTIFICATIONS_MESSAGE_DESTINATION_SES_ACCOUNT( "notifications.message_destination.ses_account", BasicCounter() ), diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/send/SendMessageActionHelper.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/send/SendMessageActionHelper.kt index 8a8d9e19..ce9c3d19 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/send/SendMessageActionHelper.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/send/SendMessageActionHelper.kt @@ -48,6 +48,7 @@ import org.opensearch.notifications.spi.model.MessageContent import org.opensearch.notifications.spi.model.destination.BaseDestination import org.opensearch.notifications.spi.model.destination.ChimeDestination import org.opensearch.notifications.spi.model.destination.CustomWebhookDestination +import org.opensearch.notifications.spi.model.destination.MicrosoftTeamsDestination import org.opensearch.notifications.spi.model.destination.SesDestination import org.opensearch.notifications.spi.model.destination.SlackDestination import org.opensearch.notifications.spi.model.destination.SmtpDestination @@ -226,12 +227,12 @@ object SendMessageActionHelper { ConfigType.NONE -> null ConfigType.SLACK -> sendSlackMessage(configData as Slack, message, eventStatus, eventSource.referenceId) ConfigType.CHIME -> sendChimeMessage(configData as Chime, message, eventStatus, eventSource.referenceId) - ConfigType.WEBHOOK -> sendWebhookMessage( - configData as Webhook, - message, - eventStatus, - eventSource.referenceId - ) +// ConfigType.WEBHOOK -> sendWebhookMessage( +// configData as Webhook, +// message, +// eventStatus, +// eventSource.referenceId +// ) ConfigType.EMAIL -> sendEmailMessage( user, configData as Email, @@ -240,6 +241,7 @@ object SendMessageActionHelper { eventStatus, eventSource.referenceId ) + ConfigType.WEBHOOK -> sendMicrosoftTeamsMessage(configData as Webhook, message, eventStatus, eventSource.referenceId) ConfigType.SES_ACCOUNT -> null ConfigType.SMTP_ACCOUNT -> null ConfigType.EMAIL_GROUP -> null @@ -336,6 +338,12 @@ object SendMessageActionHelper { LegacyDestinationResponse.Builder().withStatusCode(400) .withResponseContent("Channel type given (sns) for publishing to legacy destination not supported").build() } +// LegacyDestinationType.LEGACY_CUSTOM_WEBHOOK -> { +// val destination = MicrosoftTeamsDestination(baseMessage.url) +// val status = sendMessageThroughSpi(destination, message, "legacy") +// LegacyDestinationResponse.Builder().withStatusCode(status.statusCode) +// .withResponseContent(status.statusText).build() +// } null -> { log.warn("No channel type given (null) for publishing to legacy destination") LegacyDestinationResponse.Builder().withStatusCode(400) @@ -584,6 +592,20 @@ object SendMessageActionHelper { DestinationMessageResponse(RestStatus.FAILED_DEPENDENCY.status, "Failed to send notification") } } + /** + * Send message to destination using microsoftTeams + */ + private fun sendMicrosoftTeamsMessage( + webhook: Webhook, + message: MessageContent, + eventStatus: EventStatus, + referenceId: String + ): EventStatus { + Metrics.NOTIFICATIONS_MESSAGE_DESTINATION_MICROSOFT_TEAMS.counter.increment() + val destination = MicrosoftTeamsDestination(webhook.url) + val status = sendMessageThroughSpi(destination, message, referenceId) + return eventStatus.copy(deliveryStatus = DeliveryStatus(status.statusCode.toString(), status.statusText)) + } /** * Collects all child configs of the channel configurations (like email) diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/send/SendTestMessageRestHandlerIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/send/SendTestMessageRestHandlerIT.kt index bcbde29f..a43d77c8 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/send/SendTestMessageRestHandlerIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/send/SendTestMessageRestHandlerIT.kt @@ -115,6 +115,38 @@ internal class SendTestMessageRestHandlerIT : PluginRestTestCase() { val error = sendResponse.get("error").asJsonObject Assert.assertNotNull(error.get("reason").asString) } + @Suppress("EmptyFunctionBlock") + fun `test send test microsoft Teams message`() { + // Create webhook notification config + val createRequestJsonString = """ + { + "config":{ + "name":"this is a sample config name", + "description":"this is a sample config description", + "config_type":"webhook", + "is_enabled":true, + "chime":{ + "url":"https://8m7xqz.webhook.office.com/webhookb2/b0885113-57f8-4b61-8f3a-bdf3f4ae2831@500d1839-8666-4320-9f55-59d8838ad8db/IncomingWebhook/84637be48f4245c09b82e735b2cd9335/b7e1bf56-6634-422c-abe8-402e6e95fc68" + } + } + } + """.trimIndent() + val configId = createConfigWithRequestJsonString(createRequestJsonString) + Assert.assertNotNull(configId) + Thread.sleep(1000) + + // send test message + val sendResponse = executeRequest( + RestRequest.Method.POST.name, + "$PLUGIN_BASE_URI/feature/test/$configId", + "", + RestStatus.INTERNAL_SERVER_ERROR.status + ) + + // verify failure response is with message + val error = sendResponse.get("error").asJsonObject + Assert.assertNotNull(error.get("reason").asString) + } @Suppress("EmptyFunctionBlock") fun `test send test smtp email message`() { From 6845949c8fb9d291c6bf9452d1b6171d4e5b77e5 Mon Sep 17 00:00:00 2001 From: danielkyalo599 Date: Fri, 10 Feb 2023 17:18:55 +0300 Subject: [PATCH 2/3] Added feature support for microsoft teams webhook ,removed valid webhooks Signed-off-by: danielkyalo599 --- .../notifications/spi/utils/ValidationHelpersTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notifications/core-spi/src/test/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpersTests.kt b/notifications/core-spi/src/test/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpersTests.kt index 67a0cc06..cad3db0b 100644 --- a/notifications/core-spi/src/test/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpersTests.kt +++ b/notifications/core-spi/src/test/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpersTests.kt @@ -22,7 +22,7 @@ internal class ValidationHelpersTests { private val LOCAL_HOST_EXTENDED = "https://localhost:6060/service" private val WEBHOOK_URL = "https://test-webhook.com:1234/subdirectory?param1=value1¶m2=¶m3=value3" private val CHIME_URL = "https://domain.com/sample_chime_url#1234567890" - private val MICROSOFT_TEAMS_WEBHOOK_URL = "https://8m7xqz.webhook.office.com/webhookb2/b0885113-57f8-4b61-8f3a-bdf3f4ae2831@500d1839-8666-4320-9f55-59d8838ad8db/IncomingWebhook/84637be48f4245c09b82e735b2cd9335/b7e1bf56-6634-422c-abe8-402e6e95fc68" + private val MICROSOFT_TEAMS_WEBHOOK_URL = "https://{}.webhook.office.com/webhookb2/{}/IncomingWebhook/{}" private val hostDenyList = listOf( "127.0.0.0/8", From 077fd85334fc7697f4b0dd2f4ef929ce6bc9f6b3 Mon Sep 17 00:00:00 2001 From: danielkyalo599 Date: Fri, 10 Feb 2023 19:33:49 +0300 Subject: [PATCH 3/3] Added feature support for Microsoft teams webhook Signed-off-by: danielkyalo599 --- .../core/destinations/MicrosofTeamsDestinationTests.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/notifications/core/src/test/kotlin/org/opensearch/notifications/core/destinations/MicrosofTeamsDestinationTests.kt b/notifications/core/src/test/kotlin/org/opensearch/notifications/core/destinations/MicrosofTeamsDestinationTests.kt index 205860e3..b099ab9e 100644 --- a/notifications/core/src/test/kotlin/org/opensearch/notifications/core/destinations/MicrosofTeamsDestinationTests.kt +++ b/notifications/core/src/test/kotlin/org/opensearch/notifications/core/destinations/MicrosofTeamsDestinationTests.kt @@ -1,3 +1,10 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.notifications.core.destinations + internal class MicrosofTeamsDestinationTests { companion object { @JvmStatic