From b1d1bf24bc71ee39c40eb40cb5269395be9c6be4 Mon Sep 17 00:00:00 2001 From: Anantha Krishna Bhatta Date: Mon, 5 Oct 2020 16:35:22 -0700 Subject: [PATCH 01/29] Updated README with basic description TODO: Need to create header links for easy navigation --- README.md | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 104 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 847260ca..bb392c54 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,106 @@ -## My Project +# Notifications plugin for Open Distro -TODO: Fill this README out! +## Overview +Notifications plugin for Open Distro enables other plugins to send notifications via Email, Slack, Amazon Chime, Custom web-hook etc channels -Be sure to: +## Highlights -* Change the title in this README -* Edit your repository description on GitHub +1. Supports sending email with attachment (PDF, PNG, CSV, etc). +1. Supports sending multipart email with Text and HTML body with full Embedded HTML support. +1. Supports cross-plugin calls to send notifications (without re-implementing). +1. Supports tracking the number of email sent from this plugin and throttling based on it. + +## Documentation + +Please see our [documentation](https://opendistro.github.io/for-elasticsearch-docs/). + +## Setup + +1. Check out this package from version control. +1. Launch Intellij IDEA, choose **Import Project**, and select the `settings.gradle` file in the root of this package. +1. To build from the command line, set `JAVA_HOME` to point to a JDK >= 14 before running `./gradlew`. + +### Setup Amazon SES and SDK + +While using Amazon SES as email channel for sending mail, use below procedure for SES setup and configure environment. + +1. [Setup Amazon SES account](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/sign-up-for-aws.html) +1. [Verify Email address](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/verify-email-addresses-procedure.html) +1. [Create IAM role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_terms-and-concepts.html#iam-term-service-role-ec2) with [Allowing Access to Email-Sending Actions Only](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/control-user-access.html) `Action` required are `SendEmail` and `SendRawEmail`. +1. While using command line [configure AWS credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) [Refer Best practices](https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html) +1. [Use Amazon EC2 IAM role to grant permissions](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html) while using EC2 + +## Build + +This project uses following tools + +1. [Gradle](https://docs.gradle.org/current/userguide/userguide.html) build system. Gradle comes with an excellent documentation that should be your first stop when trying to figure out how to operate or modify the build. +1. Elastic build tools for Gradle. These tools are idiosyncratic and don't always follow the conventions and instructions for building regular Java code using Gradle. If you encounter such a situation, the Elastic build tools [source code](https://github.com/elastic/elasticsearch/tree/master/buildSrc/src/main/groovy/org/elasticsearch/gradle) is your best bet for figuring out what's going on. + +### Building from the command line + +1. `./gradlew build` builds and tests project. +1. `./gradlew run` launches a single node cluster with the `notifications` plugin installed. +1. `./gradlew run -PnumNodes=3` launches a multi-node cluster (3 nodes) with the `notifications` plugin installed. +1. `./gradlew integTest` launches a single node cluster with the `notifications` plugin installed and runs all integ tests. +1. `./gradlew integTest -PnumNodes=3` launches a multi-node cluster with the `notifications` plugin installed and runs all integ tests. +1. `./gradlew integTest -Dtests.class="*RunnerIT"` runs a single integ test class +1. `./gradlew integTest -Dtests.method="test execute * with dryrun"` runs a single integ test method + (remember to quote the test method name if it contains spaces). + +When launching a cluster using above commands, logs are placed in `notifications/build/testclusters/integTest-0/logs/`. + +#### Run integration tests with Security enabled + +1. Setup a local ODFE cluster with security plugin. +- `./gradlew build` +- `./gradlew integTest -Dtests.rest.cluster=localhost:9200 -Dtests.cluster=localhost:9200 -Dtests.clustername=es-integrationtest -Dhttps=true -Duser=admin -Dpassword=admin` +- `./gradlew integTestRunner -Dtests.rest.cluster=localhost:9200 -Dtests.cluster=localhost:9200 -Dtests.clustername=es-integrationtest -Dhttps=true -Duser=admin -Dpassword=admin --tests ""` + +### Debugging + +Sometimes it's useful to attach a debugger to either the Elasticsearch cluster, or the integ tests to see what's going on. When running unit tests, hit **Debug** from the IDE's gutter to debug the tests. +You must start your debugger to listen for remote JVM before running the below commands. + +To debug code running in an actual server, run: + +``` +./gradlew integTest -Des.debug # to start a cluster and run integ tests +``` + +OR + +``` +./gradlew run --debug-jvm # to just start a cluster that can be debugged +``` + +The Elasticsearch server JVM will launch suspended and wait for a debugger to attach to `localhost:5005` before starting the Elasticsearch server. +The IDE needs to listen for the remote JVM. If using Intellij you must set your debug-configuration to "Listen to remote JVM" and make sure "Auto Restart" is checked. +You must start your debugger to listen for remote JVM before running the commands. + +To debug code running in an integ test (which exercises the server from a separate JVM), run: + +``` +./gradlew -Dtest.debug integTest +``` + +The test runner JVM will start suspended and wait for a debugger to attach to `localhost:5005` before running the tests. + + +### Advanced: Launching multi-node clusters locally + +Sometimes you need to launch a cluster with more than one Elasticsearch server process. + +You can do this by running `./gradlew run -PnumNodes=` + +You can also run the integration tests against a multi-node cluster by running `./gradlew integTest -PnumNodes=` + +You can also debug a multi-node cluster, by using a combination of above multi-node and debug steps. +You must set up debugger configurations to listen on each port starting from `5005` and increasing by 1 for each node. + +## Code of Conduct + +See [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) for more information. ## Security @@ -13,5 +108,8 @@ See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more inform ## License -This project is licensed under the Apache-2.0 License. +This project is licensed under the Apache-2.0 License. See [LICENSE](LICENSE) for more information. + +## Notice +See [NOTICE](NOTICE) for more information. From dab3f7552363e72a89e7ed06e5435ca9049a412e Mon Sep 17 00:00:00 2001 From: Joshua Date: Mon, 19 Apr 2021 15:27:05 -0700 Subject: [PATCH 02/29] Add docs to main branch for OpenSearch (#2) * Add docs to main branch * Update pull request template Signed-off-by: Joshua Li * Addrss comments Signed-off-by: Joshua Li * Update readme from develop branch Signed-off-by: Joshua Li --- .github/ISSUE_TEMPLATE/bug_template.md | 33 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 19 ++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 16 ++++++++++ CODE_OF_CONDUCT.md | 29 +++++++++++++++--- CONTRIBUTING.md | 36 +++++++++++++++++++++++ LICENSE | 28 +++++++++++++++++- MAINTAINERS.md | 8 +++++ NOTICE | 10 ++++++- README.md | 27 +++++++++++------ 9 files changed, 191 insertions(+), 15 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_template.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 MAINTAINERS.md diff --git a/.github/ISSUE_TEMPLATE/bug_template.md b/.github/ISSUE_TEMPLATE/bug_template.md new file mode 100644 index 00000000..8fae24db --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_template.md @@ -0,0 +1,33 @@ +--- +name: 🐛 Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: bug +assignees: '' +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Plugins** +Please list all plugins currently enabled. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Host/Environment (please complete the following information):** + - OS: [e.g. iOS] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..2791b808 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: 🎆 Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..19d54289 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ +### Description +[Describe what this change achieves] + +### Issues Resolved +[List any issues this PR will resolve] + +### Check List +- [ ] New functionality includes testing. + - [ ] All tests pass, including unit test, integration test and doctest +- [ ] New functionality has been documented. + - [ ] New functionality has javadoc added + - [ ] New functionality has user manual doc added +- [ ] Commits are signed per the DCO using --signoff + +By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. +For more information on following Developer Certificate of Origin and signing off your commits, please check [here](https://github.com/opensearch-project/OpenSearch/blob/main/CONTRIBUTING.md#developer-certificate-of-origin). diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 5b627cfa..997bae66 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,4 +1,25 @@ -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. + +This code of conduct applies to all spaces provided by the OpenSource project including in code, documentation, issue trackers, mailing lists, chat channels, wikis, blogs, social media and any other communication channels used by the project. + + +**Our open source communities endeavor to:** + +* Be Inclusive: We are committed to being a community where everyone can join and contribute. This means using inclusive and welcoming language. +* Be Welcoming: We are committed to maintaining a safe space for everyone to be able to contribute. +* Be Respectful: We are committed to encouraging differing viewpoints, accepting constructive criticism and work collaboratively towards decisions that help the project grow. Disrespectful and unacceptable behavior will not be tolerated. +* Be Collaborative: We are committed to supporting what is best for our community and users. When we build anything for the benefit of the project, we should document the work we do and communicate to others on how this affects their work. + + +**Our Responsibility. As contributors, members, or bystanders we each individually have the responsibility to behave professionally and respectfully at all times. Disrespectful and unacceptable behaviors include, but are not limited to:** + +* The use of violent threats, abusive, discriminatory, or derogatory language; +* Offensive comments related to gender, gender identity and expression, sexual orientation, disability, mental illness, race, political or religious affiliation; +* Posting of sexually explicit or violent content; +* The use of sexualized language and unwelcome sexual attention or advances; +* Public or private harassment of any kind; +* Publishing private information, such as physical or electronic address, without permission; +* Other conduct which could reasonably be considered inappropriate in a professional setting; +* Advocating for or encouraging any of the above behaviors. +* Enforcement and Reporting Code of Conduct Issues: + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported. [Contact us](mailto:opensource-codeofconduct@amazon.com). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 914e0741..e1dfb00a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,6 +19,42 @@ reported the issue. Please try to include as much information as you can. Detail * Any modifications you've made relevant to the bug * Anything unusual about your environment or deployment +## Sign your work +The sign-off is a simple line at the end of each commit, which certifies that you wrote it or otherwise have the right to pass it on as an open-source patch. if you can certify the below +``` +By making a contribution to this project, I certify that: +(a) The contribution was created in whole or in part by me and I +have the right to submit it under the open source license +indicated in the file; or +(b) The contribution is based upon previous work that, to the best +of my knowledge, is covered under an appropriate open source +license and I have the right under that license to submit that +work with modifications, whether created in whole or in part +by me, under the same open source license (unless I am +permitted to submit under a different license), as indicated +in the file; or +(c) The contribution was provided directly to me by some other +person who certified (a), (b) or (c) and I have not modified +it. +(d) I understand and agree that this project and the contribution +are public and that a record of the contribution (including all +personal information I submit with it, including my sign-off) is +maintained indefinitely and may be redistributed consistent with +this project or the open source license(s) involved. +``` +then you just add a line to every git commit message: +``` +Signed-off-by: Bob Sanders +``` +You can sign off your work easily by adding the configuration in github +``` +git config user.name "Bob Sanders" +git config user.email "bob.sanders@email.com" +``` +Then, you could sign off commits automatically by adding `-s` or `-=signoff` parameter to your usual git commits commands. e.g. +``` +git commit -s -m "my first commit" +``` ## Contributing via Pull Requests Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: diff --git a/LICENSE b/LICENSE index 67db8588..261eeb9e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,3 @@ - Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -173,3 +172,30 @@ defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License 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. diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 00000000..b65d172e --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,8 @@ +# Notifications Maintainers + +## Maintainers +| Maintainer | GitHub ID | Affiliation | +|------------------------|---------------------------------------------------|-------------| +| Anantha Krishna Bhatta | [akbhatta](https://github.com/akbhatta) | Amazon | +| Joshua Li | [joshuali925](https://github.com/joshuali925) | Amazon | +| Zhongnan Su | [zhongnansu](https://github.com/zhongnansu) | Amazon | diff --git a/NOTICE b/NOTICE index 616fc588..a9cf0fc8 100644 --- a/NOTICE +++ b/NOTICE @@ -1 +1,9 @@ -Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +OpenSearch +Copyright 2021 OpenSearch Contributors + +This product includes software developed by +Elasticsearch (http://www.elastic.co). +Copyright 2009-2018 Elasticsearch + +This product includes software developed by The Apache Software +Foundation (http://www.apache.org/). diff --git a/README.md b/README.md index bb392c54..96322f9f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,10 @@ -# Notifications plugin for Open Distro +[![codecov](https://codecov.io/gh/opendistro-for-elasticsearch/notifications/branch/develop/graph/badge.svg?token=VV8JDA5DKY)](https://codecov.io/gh/opendistro-for-elasticsearch/notifications) + + +# Notifications plugin for OpenSearch ## Overview -Notifications plugin for Open Distro enables other plugins to send notifications via Email, Slack, Amazon Chime, Custom web-hook etc channels +Notifications plugin for OpenSearch enables other plugins to send notifications via Email, Slack, Amazon Chime, Custom web-hook etc channels ## Highlights @@ -20,6 +23,12 @@ Please see our [documentation](https://opendistro.github.io/for-elasticsearch-do 1. Launch Intellij IDEA, choose **Import Project**, and select the `settings.gradle` file in the root of this package. 1. To build from the command line, set `JAVA_HOME` to point to a JDK >= 14 before running `./gradlew`. +### Setup email notification using localhost email relay/server + +1. Run local email server on the machine where OpenSearch is running. e.g. for Mac, run command `sudo postfix start` +1. Verify that local email server does not require any authentication (Make sure server is listening on local port only) +1. Update the `notification.yml` configuration file according to your setup + ### Setup Amazon SES and SDK While using Amazon SES as email channel for sending mail, use below procedure for SES setup and configure environment. @@ -35,7 +44,7 @@ While using Amazon SES as email channel for sending mail, use below procedure fo This project uses following tools 1. [Gradle](https://docs.gradle.org/current/userguide/userguide.html) build system. Gradle comes with an excellent documentation that should be your first stop when trying to figure out how to operate or modify the build. -1. Elastic build tools for Gradle. These tools are idiosyncratic and don't always follow the conventions and instructions for building regular Java code using Gradle. If you encounter such a situation, the Elastic build tools [source code](https://github.com/elastic/elasticsearch/tree/master/buildSrc/src/main/groovy/org/elasticsearch/gradle) is your best bet for figuring out what's going on. +1. OpenSearch build tools for Gradle. These tools are idiosyncratic and don't always follow the conventions and instructions for building regular Java code using Gradle. If you encounter such a situation, the OpenSearch build tools is your best bet for figuring out what's going on. ### Building from the command line @@ -54,18 +63,18 @@ When launching a cluster using above commands, logs are placed in `notifications 1. Setup a local ODFE cluster with security plugin. - `./gradlew build` -- `./gradlew integTest -Dtests.rest.cluster=localhost:9200 -Dtests.cluster=localhost:9200 -Dtests.clustername=es-integrationtest -Dhttps=true -Duser=admin -Dpassword=admin` -- `./gradlew integTestRunner -Dtests.rest.cluster=localhost:9200 -Dtests.cluster=localhost:9200 -Dtests.clustername=es-integrationtest -Dhttps=true -Duser=admin -Dpassword=admin --tests ""` +- `./gradlew integTest -Dtests.rest.cluster=localhost:9200 -Dtests.cluster=localhost:9200 -Dtests.clustername=opensearch-integrationtest -Dhttps=true -Duser=admin -Dpassword=admin` +- `./gradlew integTestRunner -Dtests.rest.cluster=localhost:9200 -Dtests.cluster=localhost:9200 -Dtests.clustername=opensearch-integrationtest -Dhttps=true -Duser=admin -Dpassword=admin --tests ""` ### Debugging -Sometimes it's useful to attach a debugger to either the Elasticsearch cluster, or the integ tests to see what's going on. When running unit tests, hit **Debug** from the IDE's gutter to debug the tests. +Sometimes it's useful to attach a debugger to either the OpenSearch cluster, or the integ tests to see what's going on. When running unit tests, hit **Debug** from the IDE's gutter to debug the tests. You must start your debugger to listen for remote JVM before running the below commands. To debug code running in an actual server, run: ``` -./gradlew integTest -Des.debug # to start a cluster and run integ tests +./gradlew integTest -Dopensearch.debug # to start a cluster and run integ tests ``` OR @@ -74,7 +83,7 @@ OR ./gradlew run --debug-jvm # to just start a cluster that can be debugged ``` -The Elasticsearch server JVM will launch suspended and wait for a debugger to attach to `localhost:5005` before starting the Elasticsearch server. +The OpenSearch server JVM will launch suspended and wait for a debugger to attach to `localhost:5005` before starting the OpenSearch server. The IDE needs to listen for the remote JVM. If using Intellij you must set your debug-configuration to "Listen to remote JVM" and make sure "Auto Restart" is checked. You must start your debugger to listen for remote JVM before running the commands. @@ -89,7 +98,7 @@ The test runner JVM will start suspended and wait for a debugger to attach to `l ### Advanced: Launching multi-node clusters locally -Sometimes you need to launch a cluster with more than one Elasticsearch server process. +Sometimes you need to launch a cluster with more than one OpenSearch server process. You can do this by running `./gradlew run -PnumNodes=` From 900fc755811c4fdf54821b680ed00c03268e9a81 Mon Sep 17 00:00:00 2001 From: Vacha Shah Date: Fri, 7 May 2021 11:06:03 -0700 Subject: [PATCH 03/29] Update issue template with multiple labels Signed-off-by: Vacha Shah --- .github/ISSUE_TEMPLATE/bug_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_template.md b/.github/ISSUE_TEMPLATE/bug_template.md index 8fae24db..8af6ebb5 100644 --- a/.github/ISSUE_TEMPLATE/bug_template.md +++ b/.github/ISSUE_TEMPLATE/bug_template.md @@ -2,7 +2,7 @@ name: 🐛 Bug report about: Create a report to help us improve title: "[BUG]" -labels: bug +labels: 'bug, untriaged, Beta' assignees: '' --- From bd332ada45256d105d3ae964f52d07f1635cf6d9 Mon Sep 17 00:00:00 2001 From: Zhongnan Su Date: Sat, 31 Jul 2021 10:58:42 -0700 Subject: [PATCH 04/29] call smtp function in SPI from sendMessageAction handler (#236) Signed-off-by: Zhongnan Su --- .../send/SendMessageActionHelper.kt | 32 ++++-- .../config/QueryNotificationConfigIT.kt | 65 ++++++++++++ .../send/SendTestMessageRestHandlerIT.kt | 99 +++++++++++++++++++ 3 files changed, 189 insertions(+), 7 deletions(-) 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 d0efc5be..dbe63c52 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 @@ -29,6 +29,7 @@ import org.opensearch.commons.notifications.model.EventSource import org.opensearch.commons.notifications.model.EventStatus import org.opensearch.commons.notifications.model.NotificationEvent import org.opensearch.commons.notifications.model.Slack +import org.opensearch.commons.notifications.model.SmtpAccount import org.opensearch.commons.notifications.model.Webhook import org.opensearch.commons.utils.logger import org.opensearch.notifications.NotificationPlugin.Companion.LOG_PREFIX @@ -44,6 +45,8 @@ 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.DestinationType +import org.opensearch.notifications.spi.model.destination.EmailDestination import org.opensearch.notifications.spi.model.destination.SlackDestination import org.opensearch.rest.RestStatus import java.time.Instant @@ -151,19 +154,24 @@ object SendMessageActionHelper { childConfigs: List, message: MessageContent ): EventStatus { + val configType = channel.configDoc.config.configType + val configData = channel.configDoc.config.configData + var emailRecipientStatus = listOf() + if (configType == ConfigType.EMAIL) { + emailRecipientStatus = listOf(EmailRecipientStatus("placeholder@amazon.com", DeliveryStatus("Scheduled", "Pending execution"))) + } val eventStatus = EventStatus( channel.docInfo.id!!, // ID from query so not expected to be null channel.configDoc.config.name, channel.configDoc.config.configType, - listOf(), + emailRecipientStatus, DeliveryStatus("Scheduled", "Pending execution") ) val invalidStatus: DeliveryStatus? = getStatusIfChannelIsNotEligibleToSendMessage(eventSource, channel) if (invalidStatus != null) { return eventStatus.copy(deliveryStatus = invalidStatus) } - val configType = channel.configDoc.config.configType - val configData = channel.configDoc.config.configData + val response = when (configType) { ConfigType.NONE -> null ConfigType.SLACK -> sendSlackMessage(configData as Slack, message, eventStatus) @@ -243,7 +251,9 @@ object SendMessageActionHelper { val emailRecipientStatus: List runBlocking { val statusDeferredList = recipients.map { - async(Dispatchers.IO) { sendEmailFromSmtpAccount(smtpAccount, it, message) } + async(Dispatchers.IO) { + sendEmailFromSmtpAccount(smtpAccount?.configDoc?.config?.configData as SmtpAccount, it, message) + } } emailRecipientStatus = statusDeferredList.awaitAll() } @@ -269,14 +279,22 @@ object SendMessageActionHelper { */ @Suppress("UnusedPrivateMember") private fun sendEmailFromSmtpAccount( - smtpAccount: NotificationConfigDocInfo?, + smtpAccount: SmtpAccount, recipient: String, message: MessageContent ): EmailRecipientStatus { - // TODO implement email channel conversion + val destination = EmailDestination( + smtpAccount.host, + smtpAccount.port, + smtpAccount.method.tag, + smtpAccount.fromAddress, + recipient, + DestinationType.SMTP + ) + val status = sendMessageThroughSpi(destination, message) return EmailRecipientStatus( recipient, - DeliveryStatus(RestStatus.NOT_IMPLEMENTED.name, "SMTP Channel not implemented") + DeliveryStatus(status.statusCode.toString(), status.statusText) ) } diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/QueryNotificationConfigIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/QueryNotificationConfigIT.kt index e8c4fa9a..dbdfff1a 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/QueryNotificationConfigIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/QueryNotificationConfigIT.kt @@ -28,19 +28,23 @@ package org.opensearch.integtest.config import org.junit.Assert +import org.opensearch.commons.notifications.model.Chime import org.opensearch.commons.notifications.model.ConfigType import org.opensearch.commons.notifications.model.Feature import org.opensearch.commons.notifications.model.Feature.ALERTING import org.opensearch.commons.notifications.model.Feature.INDEX_MANAGEMENT import org.opensearch.commons.notifications.model.Feature.REPORTS +import org.opensearch.commons.notifications.model.NotificationConfig import org.opensearch.integtest.PluginRestTestCase import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI import org.opensearch.notifications.verifyMultiConfigIdEquals import org.opensearch.notifications.verifyOrderedConfigList +import org.opensearch.notifications.verifySingleConfigEquals import org.opensearch.notifications.verifySingleConfigIdEquals import org.opensearch.rest.RestRequest import org.opensearch.rest.RestStatus import java.time.Instant +import java.util.EnumSet import kotlin.random.Random class QueryNotificationConfigIT : PluginRestTestCase() { @@ -817,4 +821,65 @@ class QueryNotificationConfigIT : PluginRestTestCase() { verifyMultiConfigIdEquals(domainIds, getDomainResponse, domainIds.size) Thread.sleep(100) } + + fun `test Get single absent config should fail and then create a config using absent id should pass`() { + val absentId = "absent_id" + Thread.sleep(1000) + // Get notification config with absent id + executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/configs/$absentId", + "", + RestStatus.NOT_FOUND.status + ) + + Thread.sleep(1000) + + // Create sample config request reference + val sampleChime = Chime("https://domain.com/sample_chime_url#1234567890") + val referenceObject = NotificationConfig( + "this is a sample config name", + "this is a sample config description", + ConfigType.CHIME, + EnumSet.of(ALERTING, REPORTS), + isEnabled = true, + configData = sampleChime + ) + + // Create chime notification config + val createRequestJsonString = """ + { + "config_id":"$absentId", + "config":{ + "name":"${referenceObject.name}", + "description":"${referenceObject.description}", + "config_type":"chime", + "feature_list":[ + "${referenceObject.features.elementAt(0)}", + "${referenceObject.features.elementAt(1)}" + ], + "is_enabled":${referenceObject.isEnabled}, + "chime":{"url":"${(referenceObject.configData as Chime).url}"} + } + } + """.trimIndent() + val createResponse = executeRequest( + RestRequest.Method.POST.name, + "$PLUGIN_BASE_URI/configs", + createRequestJsonString, + RestStatus.OK.status + ) + Assert.assertEquals(absentId, createResponse.get("config_id").asString) + Thread.sleep(1000) + + // Get chime notification config + + val getConfigResponse = executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/configs/$absentId", + "", + RestStatus.OK.status + ) + verifySingleConfigEquals(absentId, referenceObject, getConfigResponse) + } } 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 22dd3f20..86f42c65 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 @@ -28,6 +28,8 @@ package org.opensearch.integtest.send import org.junit.Assert +import org.opensearch.commons.notifications.model.MethodType +import org.opensearch.commons.notifications.model.SmtpAccount import org.opensearch.integtest.PluginRestTestCase import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI import org.opensearch.rest.RestRequest @@ -143,4 +145,101 @@ internal class SendTestMessageRestHandlerIT : PluginRestTestCase() { Assert.assertNotNull(getResponseItem.get("event").asJsonObject) Thread.sleep(100) } + + @Suppress("EmptyFunctionBlock") + fun `test send test smtp email message`() { + val sampleSmtpAccount = SmtpAccount( + "localhost", + 25, + MethodType.NONE, + "szhongna@testemail.com" + ) + // Create smtp account notification config + val smtpAccountCreateRequestJsonString = """ + { + "config":{ + "name":"this is a sample smtp", + "description":"this is a sample smtp description", + "config_type":"smtp_account", + "feature_list":[ + "index_management", + "reports", + "alerting" + ], + "is_enabled":true, + "smtp_account":{ + "host":"${sampleSmtpAccount.host}", + "port":"${sampleSmtpAccount.port}", + "method":"${sampleSmtpAccount.method}", + "from_address":"${sampleSmtpAccount.fromAddress}" + } + } + } + """.trimIndent() + val createResponse = executeRequest( + RestRequest.Method.POST.name, + "$PLUGIN_BASE_URI/configs", + smtpAccountCreateRequestJsonString, + RestStatus.OK.status + ) + val smtpAccountConfigId = createResponse.get("config_id").asString + Assert.assertNotNull(smtpAccountConfigId) + Thread.sleep(1000) + + val emailCreateRequestJsonString = """ + { + "config":{ + "name":"email config name", + "description":"email description", + "config_type":"email", + "feature_list":[ + "index_management", + "reports", + "alerting" + ], + "is_enabled":true, + "email":{ + "email_account_id":"$smtpAccountConfigId", + "recipient_list":[ + "chloe@example.com" + ], + "email_group_id_list":[] + } + } + } + """.trimIndent() + + val emailCreateResponse = executeRequest( + RestRequest.Method.POST.name, + "$PLUGIN_BASE_URI/configs", + emailCreateRequestJsonString, + RestStatus.OK.status + ) + val emailConfigId = emailCreateResponse.get("config_id").asString + Assert.assertNotNull(emailConfigId) + Thread.sleep(1000) + + // send test message + val sendResponse = executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/feature/test/$emailConfigId?feature=alerting", + "", + RestStatus.OK.status + ) + val eventId = sendResponse.get("event_id").asString + + val getEventResponse = executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/events/$eventId", + "", + RestStatus.OK.status + ) + val items = getEventResponse.get("event_list").asJsonArray + Assert.assertEquals(1, items.size()) + val getResponseItem = items[0].asJsonObject + Assert.assertEquals(eventId, getResponseItem.get("event_id").asString) + Assert.assertEquals("", getResponseItem.get("tenant").asString) + Assert.assertNotNull(getResponseItem.get("event").asJsonObject) + Thread.sleep(100) + } } From 5cad6a6a52ae29dccd4d5e1e353ce9067ca39b1d Mon Sep 17 00:00:00 2001 From: Zhongnan Su Date: Mon, 2 Aug 2021 13:43:58 -0700 Subject: [PATCH 05/29] Support HOST_DENY_LIST and TOOLTIP_SUPPORT in plugin settings (#250) * add tooltip_support to spi settings * add host deny list in setting and apply to url validator --- notifications/spi/build.gradle | 2 +- .../model/destination/WebhookDestination.kt | 15 +--- .../spi/setting/PluginSettings.kt | 82 +++++++++++++++++-- .../spi/utils/ValidationHelpers.kt | 25 ++++-- .../spi/utils/ValidationHelpersTests.kt | 51 ++++++++++++ 5 files changed, 150 insertions(+), 25 deletions(-) create mode 100644 notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpersTests.kt diff --git a/notifications/spi/build.gradle b/notifications/spi/build.gradle index 68226dcf..44f92537 100644 --- a/notifications/spi/build.gradle +++ b/notifications/spi/build.gradle @@ -109,7 +109,7 @@ dependencies { // exclude module: 'annotations' // conflict with org.jetbrains:annotations, integTestRunner fails with error "codebase property already set" // } compile "com.sun.mail:javax.mail:1.6.2" - + implementation "com.github.seancfoley:ipaddress:5.3.3" testImplementation( 'org.assertj:assertj-core:3.16.1', 'org.junit.jupiter:junit-jupiter-api:5.6.2', diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/WebhookDestination.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/WebhookDestination.kt index c64cea21..f0ab5cf0 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/WebhookDestination.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/WebhookDestination.kt @@ -11,10 +11,8 @@ package org.opensearch.notifications.spi.model.destination -import org.apache.http.client.utils.URIBuilder +import org.opensearch.notifications.spi.setting.PluginSettings import org.opensearch.notifications.spi.utils.validateUrl -import java.net.URI -import java.net.URISyntaxException /** * This class holds the contents of generic webbook destination @@ -25,16 +23,7 @@ abstract class WebhookDestination( ) : BaseDestination(destinationType) { init { - validateUrl(url) - } - - @SuppressWarnings("SwallowedException") - internal fun buildUri(): URI { - return try { - URIBuilder(url).build() - } catch (exception: URISyntaxException) { - throw IllegalStateException("Error creating URI") - } + validateUrl(url, PluginSettings.hostDenyList) } override fun toString(): String { diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/setting/PluginSettings.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/setting/PluginSettings.kt index 9b3b86f8..ac8e9046 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/setting/PluginSettings.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/setting/PluginSettings.kt @@ -74,6 +74,16 @@ internal object PluginSettings { */ private const val ALLOWED_CONFIG_TYPE_KEY = "$KEY_PREFIX.allowedConfigTypes" + /** + * Setting to enable tooltip in UI + */ + private const val TOOLTIP_SUPPORT_KEY = "$KEY_PREFIX.tooltip_support" + + /** + * Setting to enable tooltip in UI + */ + private const val HOST_DENY_LIST_KEY = "$EMAIL_KEY_PREFIX.host_deny_list" + /** * Default email size limit as 10MB. */ @@ -121,6 +131,16 @@ internal object PluginSettings { "email_group" ) + /** + * Default email host deny list + */ + private val DEFAULT_HOST_DENY_LIST = emptyList() + + /** + * Default disable tooltip support + */ + private const val DEFAULT_TOOLTIP_SUPPORT = false + /** * list of allowed config types. */ @@ -163,6 +183,18 @@ internal object PluginSettings { @Volatile var socketTimeout: Int + /** + * Tooltip support + */ + @Volatile + var tooltipSupport: Boolean + + /** + * list of allowed config types. + */ + @Volatile + var hostDenyList: List + private const val DECIMAL_RADIX: Int = 10 private val log by logger(javaClass) @@ -190,6 +222,8 @@ internal object PluginSettings { ?: DEFAULT_CONNECTION_TIMEOUT_MILLISECONDS socketTimeout = (settings?.get(SOCKET_TIMEOUT_MILLISECONDS_KEY)?.toInt()) ?: DEFAULT_SOCKET_TIMEOUT_MILLISECONDS allowedConfigTypes = settings?.getAsList(ALLOWED_CONFIG_TYPE_KEY, null) ?: DEFAULT_ALLOWED_CONFIG_TYPES + tooltipSupport = settings?.getAsBoolean(TOOLTIP_SUPPORT_KEY, false) ?: DEFAULT_TOOLTIP_SUPPORT + hostDenyList = settings?.getAsList(HOST_DENY_LIST_KEY, null) ?: DEFAULT_HOST_DENY_LIST defaultSettings = mapOf( EMAIL_SIZE_LIMIT_KEY to emailSizeLimit.toString(DECIMAL_RADIX), @@ -197,7 +231,8 @@ internal object PluginSettings { MAX_CONNECTIONS_KEY to maxConnections.toString(DECIMAL_RADIX), MAX_CONNECTIONS_PER_ROUTE_KEY to maxConnectionsPerRoute.toString(DECIMAL_RADIX), CONNECTION_TIMEOUT_MILLISECONDS_KEY to connectionTimeout.toString(DECIMAL_RADIX), - SOCKET_TIMEOUT_MILLISECONDS_KEY to socketTimeout.toString(DECIMAL_RADIX) + SOCKET_TIMEOUT_MILLISECONDS_KEY to socketTimeout.toString(DECIMAL_RADIX), + TOOLTIP_SUPPORT_KEY to tooltipSupport.toString() ) } @@ -245,6 +280,19 @@ internal object PluginSettings { NodeScope, Dynamic ) + private val TOOLTIP_SUPPORT: Setting = Setting.boolSetting( + TOOLTIP_SUPPORT_KEY, + defaultSettings[TOOLTIP_SUPPORT_KEY]!!.toBoolean(), + NodeScope, Dynamic + ) + + private val HOST_DENY_LIST: Setting> = Setting.listSetting( + HOST_DENY_LIST_KEY, + DEFAULT_HOST_DENY_LIST, + { it }, + NodeScope, Dynamic + ) + /** * Returns list of additional settings available specific to this plugin. * @@ -258,7 +306,9 @@ internal object PluginSettings { MAX_CONNECTIONS_PER_ROUTE, CONNECTION_TIMEOUT_MILLISECONDS, SOCKET_TIMEOUT_MILLISECONDS, - ALLOWED_CONFIG_TYPES + ALLOWED_CONFIG_TYPES, + TOOLTIP_SUPPORT, + HOST_DENY_LIST ) } /** @@ -273,6 +323,8 @@ internal object PluginSettings { maxConnectionsPerRoute = MAX_CONNECTIONS_PER_ROUTE.get(clusterService.settings) connectionTimeout = CONNECTION_TIMEOUT_MILLISECONDS.get(clusterService.settings) socketTimeout = SOCKET_TIMEOUT_MILLISECONDS.get(clusterService.settings) + tooltipSupport = TOOLTIP_SUPPORT.get(clusterService.settings) + hostDenyList = HOST_DENY_LIST.get(clusterService.settings) } /** @@ -311,10 +363,20 @@ internal object PluginSettings { log.debug("$LOG_PREFIX:$SOCKET_TIMEOUT_MILLISECONDS_KEY -autoUpdatedTo-> $clusterSocketTimeout") socketTimeout = clusterSocketTimeout } - val clusterallowedConfigTypes = clusterService.clusterSettings.get(ALLOWED_CONFIG_TYPES) - if (clusterallowedConfigTypes != null) { - log.debug("$LOG_PREFIX:$ALLOWED_CONFIG_TYPE_KEY -autoUpdatedTo-> $clusterallowedConfigTypes") - allowedConfigTypes = clusterallowedConfigTypes + val clusterAllowedConfigTypes = clusterService.clusterSettings.get(ALLOWED_CONFIG_TYPES) + if (clusterAllowedConfigTypes != null) { + log.debug("$LOG_PREFIX:$ALLOWED_CONFIG_TYPE_KEY -autoUpdatedTo-> $clusterAllowedConfigTypes") + allowedConfigTypes = clusterAllowedConfigTypes + } + val clusterTooltipSupport = clusterService.clusterSettings.get(TOOLTIP_SUPPORT) + if (clusterTooltipSupport != null) { + log.debug("$LOG_PREFIX:$TOOLTIP_SUPPORT_KEY -autoUpdatedTo-> $clusterAllowedConfigTypes") + tooltipSupport = clusterTooltipSupport + } + val clusterHostDenyList = clusterService.clusterSettings.get(HOST_DENY_LIST) + if (clusterHostDenyList != null) { + log.debug("$LOG_PREFIX:$HOST_DENY_LIST_KEY -autoUpdatedTo-> $clusterHostDenyList") + hostDenyList = clusterHostDenyList } } @@ -356,5 +418,13 @@ internal object PluginSettings { socketTimeout = it log.info("$LOG_PREFIX:$SOCKET_TIMEOUT_MILLISECONDS_KEY -updatedTo-> $it") } + clusterService.clusterSettings.addSettingsUpdateConsumer(TOOLTIP_SUPPORT) { + tooltipSupport = it + log.info("$LOG_PREFIX:$TOOLTIP_SUPPORT_KEY -updatedTo-> $it") + } + clusterService.clusterSettings.addSettingsUpdateConsumer(HOST_DENY_LIST) { + hostDenyList = it + log.info("$LOG_PREFIX:$HOST_DENY_LIST_KEY -updatedTo-> $it") + } } } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpers.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpers.kt index c01a295e..466f1034 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpers.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpers.kt @@ -27,18 +27,19 @@ package org.opensearch.notifications.spi.utils +import inet.ipaddr.IPAddressString import org.apache.http.client.methods.HttpPatch import org.apache.http.client.methods.HttpPost import org.apache.http.client.methods.HttpPut import org.opensearch.common.Strings import java.net.URL -fun validateUrl(urlString: String) { +fun validateUrl(urlString: String, hostDenyList: List) { require(!Strings.isNullOrEmpty(urlString)) { "url is null or empty" } require(isValidUrl(urlString)) { "Invalid URL or unsupported" } - val url = URL(urlString) - require("https" == url.protocol) // Support only HTTPS. HTTP and other protocols not supported - // TODO : Add hosts deny list + require(!isHostInDenylist(urlString, hostDenyList)) { + "Host of url is denied, based on plugin setting [notification.spi.email.host_deny_list]" + } } fun validateEmail(email: String) { @@ -48,10 +49,24 @@ fun validateEmail(email: String) { fun isValidUrl(urlString: String): Boolean { val url = URL(urlString) // throws MalformedURLException if URL is invalid - // TODO : Add hosts deny list return ("https" == url.protocol) // Support only HTTPS. HTTP and other protocols not supported } +fun isHostInDenylist(urlString: String, hostDenyList: List): Boolean { + val url = URL(urlString) + if (url.host != null) { + val ipStr = IPAddressString(url.host) + for (network in hostDenyList) { + val netStr = IPAddressString(network) + if (netStr.contains(ipStr)) { + return true + } + } + } + + return false +} + /** * RFC 5322 compliant pattern matching: https://www.ietf.org/rfc/rfc5322.txt * Regex was based off of this post: https://stackoverflow.com/a/201378 diff --git a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpersTests.kt b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpersTests.kt new file mode 100644 index 00000000..5198bae2 --- /dev/null +++ b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpersTests.kt @@ -0,0 +1,51 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.spi.utils + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +internal class ValidationHelpersTests { + + private val hostDentyList = listOf( + "127.0.0.0/8", + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "0.0.0.0/8", + "9.9.9.9" // ip + ) + + @Test + fun `test ips in denylist`() { + val ips = listOf( + "127.0.0.1", // 127.0.0.0/8 + "10.0.0.1", // 10.0.0.0/8 + "10.11.12.13", // 10.0.0.0/8 + "172.16.0.1", // "172.16.0.0/12" + "192.168.0.1", // 192.168.0.0/16" + "0.0.0.1", // 0.0.0.0/8 + "9.9.9.9" + ) + for (ip in ips) { + assertEquals(true, isHostInDenylist("https://$ip", hostDentyList)) + } + } + + @Test + fun `test url in denylist`() { + val urls = listOf("https://www.amazon.com", "https://mytest.com", "https://mytest.com") + for (url in urls) { + assertEquals(false, isHostInDenylist(url, hostDentyList)) + } + } +} From a8fa9594b9e6dcb2fa32aab04aa9d59ddea054f6 Mon Sep 17 00:00:00 2001 From: Zhongnan Su Date: Mon, 2 Aug 2021 18:11:34 -0700 Subject: [PATCH 06/29] Fix custom webhook request body format (#248) * Fix custom webhook request body format * support http protocol for webhook url --- .../notifications-test-and-build-workflow.yml | 2 +- .../send/SendMessageActionHelper.kt | 2 +- .../send/SendTestMessageRestHandlerIT.kt | 58 +++++++++++++++++++ .../spi/client/DestinationHttpClient.kt | 28 ++++++--- .../notifications/spi/model/MessageContent.kt | 2 +- .../spi/utils/ValidationHelpers.kt | 2 +- .../spi/ChimeDestinationTests.kt | 26 ++++----- .../spi/CustomWebhookDestinationTests.kt | 49 ++++++++-------- .../spi/SlackDestinationTests.kt | 16 ++--- 9 files changed, 122 insertions(+), 63 deletions(-) diff --git a/.github/workflows/notifications-test-and-build-workflow.yml b/.github/workflows/notifications-test-and-build-workflow.yml index e06126dd..15799e94 100644 --- a/.github/workflows/notifications-test-and-build-workflow.yml +++ b/.github/workflows/notifications-test-and-build-workflow.yml @@ -27,7 +27,7 @@ on: [push, pull_request] env: OPENSEARCH_VERSION: '1.0' - COMMON_UTILS_VERSION: '1.0' + COMMON_UTILS_VERSION: 'main' jobs: build: 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 dbe63c52..c1ce01f1 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 @@ -230,7 +230,7 @@ object SendMessageActionHelper { * send message to custom webhook destination */ private fun sendWebhookMessage(webhook: Webhook, message: MessageContent, eventStatus: EventStatus): EventStatus { - val destination = CustomWebhookDestination(webhook.url, webhook.headerParams, "POST") + val destination = CustomWebhookDestination(webhook.url, webhook.headerParams, webhook.method.tag) val status = sendMessageThroughSpi(destination, message) return eventStatus.copy(deliveryStatus = DeliveryStatus(status.statusCode.toString(), status.statusText)) } 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 86f42c65..f7fd71e8 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 @@ -146,6 +146,64 @@ internal class SendTestMessageRestHandlerIT : PluginRestTestCase() { Thread.sleep(100) } + @Suppress("EmptyFunctionBlock") + fun `test send custom webhook 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", + "feature_list":[ + "index_management", + "reports", + "alerting" + ], + "is_enabled":true, + "webhook":{ + "url":"https://xxx.com/my-webhook@dev", + "header_params": { + "Content-type": "text/plain" + } + } + } + } + """.trimIndent() + val createResponse = executeRequest( + RestRequest.Method.POST.name, + "$PLUGIN_BASE_URI/configs", + createRequestJsonString, + RestStatus.OK.status + ) + val configId = createResponse.get("config_id").asString + Assert.assertNotNull(configId) + Thread.sleep(1000) + + // send test message + val sendResponse = executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/feature/test/$configId?feature=alerting", + "", + RestStatus.OK.status + ) + val eventId = sendResponse.get("event_id").asString + + val getEventResponse = executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/events/$eventId", + "", + RestStatus.OK.status + ) + val items = getEventResponse.get("event_list").asJsonArray + Assert.assertEquals(1, items.size()) + val getResponseItem = items[0].asJsonObject + Assert.assertEquals(eventId, getResponseItem.get("event_id").asString) + Assert.assertEquals("", getResponseItem.get("tenant").asString) + Assert.assertNotNull(getResponseItem.get("event").asJsonObject) + Thread.sleep(100) + } + @Suppress("EmptyFunctionBlock") fun `test send test smtp email message`() { val sampleSmtpAccount = SmtpAccount( diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationHttpClient.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationHttpClient.kt index 7ebeb85b..76fdc4e6 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationHttpClient.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationHttpClient.kt @@ -45,6 +45,7 @@ import org.apache.http.util.EntityUtils import org.opensearch.common.xcontent.XContentFactory import org.opensearch.common.xcontent.XContentType import org.opensearch.notifications.spi.model.MessageContent +import org.opensearch.notifications.spi.model.destination.ChimeDestination import org.opensearch.notifications.spi.model.destination.CustomWebhookDestination import org.opensearch.notifications.spi.model.destination.SlackDestination import org.opensearch.notifications.spi.model.destination.WebhookDestination @@ -127,7 +128,7 @@ class DestinationHttpClient { var httpRequest: HttpRequestBase = HttpPost(destination.url) if (destination is CustomWebhookDestination) { - httpRequest = constructHttpRequest(destination.method) + httpRequest = constructHttpRequest(destination.method, destination.url) if (destination.headerParams.isEmpty()) { // set default header httpRequest.setHeader("Content-type", "application/json") @@ -142,11 +143,11 @@ class DestinationHttpClient { return httpClient.execute(httpRequest) } - private fun constructHttpRequest(method: String): HttpRequestBase { + private fun constructHttpRequest(method: String, url: String): HttpRequestBase { return when (method) { - HttpPost.METHOD_NAME -> HttpPost() - HttpPut.METHOD_NAME -> HttpPut() - HttpPatch.METHOD_NAME -> HttpPatch() + HttpPost.METHOD_NAME -> HttpPost(url) + HttpPut.METHOD_NAME -> HttpPut(url) + HttpPatch.METHOD_NAME -> HttpPatch(url) else -> throw IllegalArgumentException( "Invalid or empty method supplied. Only POST, PUT and PATCH are allowed" ) @@ -171,11 +172,20 @@ class DestinationHttpClient { fun buildRequestBody(destination: WebhookDestination, message: MessageContent): String { val builder = XContentFactory.contentBuilder(XContentType.JSON) - var keyName = "Content" - // Slack webhook request body has required "text" as key name https://api.slack.com/messaging/webhooks - if (destination is SlackDestination) keyName = "text" + val keyName = when (destination) { + // Slack webhook request body has required "text" as key name https://api.slack.com/messaging/webhooks + // Chime webhook request body has required "Content" as key name + // Customer webhook allows input as json or plain text, so we just return the message as it is + is SlackDestination -> "text" + is ChimeDestination -> "Content" + is CustomWebhookDestination -> return message.textDescription + else -> throw IllegalArgumentException( + "Invalid destination type is provided, Only Slack, Chime and CustomWebook are allowed" + ) + } + builder.startObject() - .field(keyName, message.buildWebhookMessage()) + .field(keyName, message.buildMessageWithTitle()) .endObject() return builder.string() } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/MessageContent.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/MessageContent.kt index 64711ef0..9e9889cb 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/MessageContent.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/MessageContent.kt @@ -48,7 +48,7 @@ class MessageContent( require(!Strings.isNullOrEmpty(textDescription)) { "text message part is null or empty" } } - fun buildWebhookMessage(): String { + fun buildMessageWithTitle(): String { return "$title\n\n$textDescription" } } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpers.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpers.kt index 466f1034..6a0636a0 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpers.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpers.kt @@ -49,7 +49,7 @@ fun validateEmail(email: String) { fun isValidUrl(urlString: String): Boolean { val url = URL(urlString) // throws MalformedURLException if URL is invalid - return ("https" == url.protocol) // Support only HTTPS. HTTP and other protocols not supported + return ("https" == url.protocol || "http" == url.protocol) // Support only http/https, other protocols not supported } fun isHostInDenylist(urlString: String, hostDenyList: List): Boolean { diff --git a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/ChimeDestinationTests.kt b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/ChimeDestinationTests.kt index b5efc43d..7c1d0dc7 100644 --- a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/ChimeDestinationTests.kt +++ b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/ChimeDestinationTests.kt @@ -33,8 +33,9 @@ import org.apache.http.entity.StringEntity import org.apache.http.impl.client.CloseableHttpClient import org.apache.http.message.BasicStatusLine import org.easymock.EasyMock -import org.junit.Test +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments @@ -64,7 +65,6 @@ internal class ChimeDestinationTests { } @Test - @Throws(Exception::class) fun `test chime message null entity response`() { val mockHttpClient: CloseableHttpClient = EasyMock.createMock(CloseableHttpClient::class.java) @@ -102,7 +102,6 @@ internal class ChimeDestinationTests { } @Test - @Throws(Exception::class) fun `test chime message empty entity response`() { val mockHttpClient: CloseableHttpClient = EasyMock.createMock(CloseableHttpClient::class.java) val expectedWebhookResponse = DestinationMessageResponse(RestStatus.OK.status, "") @@ -137,7 +136,6 @@ internal class ChimeDestinationTests { } @Test - @Throws(Exception::class) fun `test chime message non-empty entity response`() { val responseContent = "It worked!" val mockHttpClient: CloseableHttpClient = EasyMock.createMock(CloseableHttpClient::class.java) @@ -172,14 +170,12 @@ internal class ChimeDestinationTests { assertEquals(expectedWebhookResponse.statusCode, actualChimeResponse.statusCode) } - @Test(expected = IllegalArgumentException::class) - fun testUrlMissingMessage() { - try { + @Test + fun `test url missing should throw IllegalArgumentException with message`() { + val exception = Assertions.assertThrows(IllegalArgumentException::class.java) { ChimeDestination("") - } catch (ex: Exception) { - assertEquals("url is null or empty", ex.message) - throw ex } + assertEquals("url is null or empty", exception.message) } @Test @@ -189,14 +185,12 @@ internal class ChimeDestinationTests { } } - @Test(expected = IllegalArgumentException::class) - fun testContentMissingMessage() { - try { + @Test + fun `test content missing content should throw IllegalArgumentException`() { + val exception = Assertions.assertThrows(IllegalArgumentException::class.java) { MessageContent("title", "") - } catch (ex: Exception) { - assertEquals("text message part is null or empty", ex.message) - throw ex } + assertEquals("text message part is null or empty", exception.message) } @ParameterizedTest diff --git a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/CustomWebhookDestinationTests.kt b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/CustomWebhookDestinationTests.kt index c474837d..e2b6c952 100644 --- a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/CustomWebhookDestinationTests.kt +++ b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/CustomWebhookDestinationTests.kt @@ -36,8 +36,9 @@ import org.apache.http.entity.StringEntity import org.apache.http.impl.client.CloseableHttpClient import org.apache.http.message.BasicStatusLine import org.easymock.EasyMock -import org.junit.Test +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments @@ -116,7 +117,6 @@ internal class CustomWebhookDestinationTests { @ParameterizedTest(name = "method {0} should return corresponding type of Http request object {1}") @MethodSource("methodToHttpRequestType") - @Throws(Exception::class) fun `test custom webhook message empty entity response`(method: String, expectedHttpClass: Class) { val mockHttpClient: CloseableHttpClient = EasyMock.createMock(CloseableHttpClient::class.java) val expectedWebhookResponse = DestinationMessageResponse(RestStatus.OK.status, "") @@ -156,7 +156,6 @@ internal class CustomWebhookDestinationTests { @ParameterizedTest(name = "method {0} should return corresponding type of Http request object {1}") @MethodSource("methodToHttpRequestType") - @Throws(Exception::class) fun `test custom webhook message non-empty entity response`( method: String, expectedHttpClass: Class @@ -197,47 +196,49 @@ internal class CustomWebhookDestinationTests { assertEquals(expectedWebhookResponse.statusCode, actualCustomWebhookResponse.statusCode) } - @Test(expected = IllegalArgumentException::class) + @ParameterizedTest(name = "method {0} should return corresponding type of Http request object {1}") + @MethodSource("methodToHttpRequestType") fun `Test missing url will throw exception`(method: String) { - try { + val exception = Assertions.assertThrows(IllegalArgumentException::class.java) { CustomWebhookDestination("", mapOf("headerKey" to "headerValue"), method) - } catch (ex: Exception) { - assertEquals("url is null or empty", ex.message) - throw ex } + assertEquals("url is null or empty", exception.message) } - @Test - fun testUrlInvalidMessage(method: String) { + @ParameterizedTest(name = "method {0} should return corresponding type of Http request object {1}") + @MethodSource("methodToHttpRequestType") + fun `Custom webhook should throw exception if url is invalid`(method: String) { assertThrows { CustomWebhookDestination("invalidUrl", mapOf("headerKey" to "headerValue"), method) } } - @Test(expected = IllegalArgumentException::class) + @ParameterizedTest(name = "method {0} should return corresponding type of Http request object {1}") + @MethodSource("methodToHttpRequestType") + fun `Custom webhook should throw exception if url protocol is not http or https`(method: String) { + val exception = Assertions.assertThrows(IllegalArgumentException::class.java) { + CustomWebhookDestination("ftp://abc/com", mapOf("headerKey" to "headerValue"), method) + } + assertEquals("Invalid URL or unsupported", exception.message) + } + + @Test fun `Test invalid method type will throw exception`() { - try { + val exception = Assertions.assertThrows(IllegalArgumentException::class.java) { CustomWebhookDestination("https://abc/com", mapOf("headerKey" to "headerValue"), "GET") - } catch (ex: Exception) { - assertEquals("Invalid method supplied. Only POST, PUT and PATCH are allowed", ex.message) - throw ex } + assertEquals("Invalid method supplied. Only POST, PUT and PATCH are allowed", exception.message) } - @ParameterizedTest - @MethodSource("escapeSequenceToRaw") - fun `test build request body for custom webhook should have title included and prevent escape`( - escapeSequence: String, - rawString: String - ) { + @Test + fun `test build request body for custom webhook`() { val httpClient = DestinationHttpClient() val title = "test custom webhook" - val messageText = "line1${escapeSequence}line2" + val messageText = "{\"Customized Key\":\"some content\"}" val url = "https://abc/com" - val expectedRequestBody = """{"Content":"$title\n\nline1${rawString}line2"}""" val destination = CustomWebhookDestination(url, mapOf("headerKey" to "headerValue"), "POST") val message = MessageContent(title, messageText) val actualRequestBody = httpClient.buildRequestBody(destination, message) - assertEquals(expectedRequestBody, actualRequestBody) + assertEquals(messageText, actualRequestBody) } } diff --git a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/SlackDestinationTests.kt b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/SlackDestinationTests.kt index a6ff6a93..e0a88a4c 100644 --- a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/SlackDestinationTests.kt +++ b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/SlackDestinationTests.kt @@ -33,8 +33,9 @@ import org.apache.http.entity.StringEntity import org.apache.http.impl.client.CloseableHttpClient import org.apache.http.message.BasicStatusLine import org.easymock.EasyMock -import org.junit.Test +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments @@ -65,7 +66,6 @@ internal class SlackDestinationTests { } @Test - @Throws(Exception::class) fun `test Slack message null entity response`() { val mockHttpClient: CloseableHttpClient = EasyMock.createMock(CloseableHttpClient::class.java) @@ -103,7 +103,6 @@ internal class SlackDestinationTests { } @Test - @Throws(Exception::class) fun `test Slack message empty entity response`() { val mockHttpClient: CloseableHttpClient = EasyMock.createMock(CloseableHttpClient::class.java) val expectedWebhookResponse = DestinationMessageResponse(RestStatus.OK.status, "") @@ -138,7 +137,6 @@ internal class SlackDestinationTests { } @Test - @Throws(Exception::class) fun `test Slack message non-empty entity response`() { val responseContent = "It worked!" val mockHttpClient: CloseableHttpClient = EasyMock.createMock(CloseableHttpClient::class.java) @@ -173,14 +171,12 @@ internal class SlackDestinationTests { assertEquals(expectedWebhookResponse.statusCode, actualSlackResponse.statusCode) } - @Test(expected = IllegalArgumentException::class) - fun testUrlMissingMessage() { - try { + @Test + fun `test url missing should throw IllegalArgumentException with message`() { + val exception = Assertions.assertThrows(IllegalArgumentException::class.java) { SlackDestination("") - } catch (ex: Exception) { - assertEquals("url is null or empty", ex.message) - throw ex } + assertEquals("url is null or empty", exception.message) } @Test From 969222b821d6d85db6aa126ed1fea969b8791454 Mon Sep 17 00:00:00 2001 From: Anantha Krishna Bhatta Date: Wed, 4 Aug 2021 16:04:49 -0700 Subject: [PATCH 07/29] Refactor SPI for SES channel addition [Tests] All unit and integration test passed Signed-off-by: @akbhatta --- .../send/SendMessageActionHelper.kt | 8 ++-- .../notifications/spi/NotificationSpi.kt | 4 +- .../spi/client/DestinationClientPool.kt | 2 +- ...mailClient.kt => DestinationSmtpClient.kt} | 16 +++---- .../spi/client/EmailMimeProvider.kt | 10 ++-- .../spi/factory/DestinationFactoryProvider.kt | 41 ---------------- .../destination/CustomWebhookDestination.kt | 2 +- .../spi/model/destination/DestinationType.kt | 2 +- ...EmailDestination.kt => SmtpDestination.kt} | 22 ++++----- .../DestinationTransport.kt} | 4 +- .../transport/DestinationTransportProvider.kt | 47 +++++++++++++++++++ .../SmtpDestinationTransport.kt} | 20 ++++---- .../WebhookDestinationTransport.kt} | 6 +-- .../spi/ChimeDestinationTests.kt | 16 +++---- .../spi/CustomWebhookDestinationTests.kt | 22 ++++----- .../spi/SlackDestinationTests.kt | 16 +++---- ...nationTests.kt => SmtpDestinationTests.kt} | 35 +++++++------- .../spi/integTest/SmtpEmailIT.kt | 17 +++---- 18 files changed, 142 insertions(+), 148 deletions(-) rename notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/{DestinationEmailClient.kt => DestinationSmtpClient.kt} (89%) delete mode 100644 notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/factory/DestinationFactoryProvider.kt rename notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/{EmailDestination.kt => SmtpDestination.kt} (67%) rename notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/{factory/DestinationFactory.kt => transport/DestinationTransport.kt} (89%) create mode 100644 notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransportProvider.kt rename notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/{factory/SmtpEmailDestinationFactory.kt => transport/SmtpDestinationTransport.kt} (77%) rename notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/{factory/WebhookDestinationFactory.kt => transport/WebhookDestinationTransport.kt} (89%) rename notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/{EmailDestinationTests.kt => SmtpDestinationTests.kt} (73%) 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 c1ce01f1..d7a93d11 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 @@ -45,9 +45,8 @@ 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.DestinationType -import org.opensearch.notifications.spi.model.destination.EmailDestination import org.opensearch.notifications.spi.model.destination.SlackDestination +import org.opensearch.notifications.spi.model.destination.SmtpDestination import org.opensearch.rest.RestStatus import java.time.Instant @@ -283,13 +282,12 @@ object SendMessageActionHelper { recipient: String, message: MessageContent ): EmailRecipientStatus { - val destination = EmailDestination( + val destination = SmtpDestination( smtpAccount.host, smtpAccount.port, smtpAccount.method.tag, smtpAccount.fromAddress, - recipient, - DestinationType.SMTP + recipient ) val status = sendMessageThroughSpi(destination, message) return EmailRecipientStatus( diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/NotificationSpi.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/NotificationSpi.kt index e3f9769c..4b9513db 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/NotificationSpi.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/NotificationSpi.kt @@ -27,11 +27,11 @@ package org.opensearch.notifications.spi -import org.opensearch.notifications.spi.factory.DestinationFactoryProvider import org.opensearch.notifications.spi.model.DestinationMessageResponse import org.opensearch.notifications.spi.model.MessageContent import org.opensearch.notifications.spi.model.destination.BaseDestination import org.opensearch.notifications.spi.setting.PluginSettings +import org.opensearch.notifications.spi.transport.DestinationTransportProvider import java.security.AccessController import java.security.PrivilegedAction @@ -49,7 +49,7 @@ object NotificationSpi { fun sendMessage(destination: BaseDestination, message: MessageContent): DestinationMessageResponse { return AccessController.doPrivileged( PrivilegedAction { - val destinationFactory = DestinationFactoryProvider.getFactory(destination.destinationType) + val destinationFactory = DestinationTransportProvider.getTransport(destination.destinationType) destinationFactory.sendMessage(destination, message) } as PrivilegedAction? ) diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationClientPool.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationClientPool.kt index a604cee1..84ebc44d 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationClientPool.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationClientPool.kt @@ -32,5 +32,5 @@ package org.opensearch.notifications.spi.client */ internal object DestinationClientPool { val httpClient: DestinationHttpClient = DestinationHttpClient() - val emailClient: DestinationEmailClient = DestinationEmailClient() + val smtpClient: DestinationSmtpClient = DestinationSmtpClient() } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationEmailClient.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSmtpClient.kt similarity index 89% rename from notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationEmailClient.kt rename to notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSmtpClient.kt index 2ab2d45f..08312913 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationEmailClient.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSmtpClient.kt @@ -14,7 +14,7 @@ package org.opensearch.notifications.spi.client import com.sun.mail.util.MailConnectException import org.opensearch.notifications.spi.model.DestinationMessageResponse import org.opensearch.notifications.spi.model.MessageContent -import org.opensearch.notifications.spi.model.destination.EmailDestination +import org.opensearch.notifications.spi.model.destination.SmtpDestination import org.opensearch.notifications.spi.setting.PluginSettings import org.opensearch.notifications.spi.utils.SecurityAccess import org.opensearch.notifications.spi.utils.logger @@ -30,14 +30,14 @@ import javax.mail.internet.MimeMessage /** * This class handles the connections to the given Destination. */ -class DestinationEmailClient { +class DestinationSmtpClient { companion object { - private val log by logger(DestinationEmailClient::class.java) + private val log by logger(DestinationSmtpClient::class.java) } @Throws(Exception::class) - fun execute(emailDestination: EmailDestination, message: MessageContent): DestinationMessageResponse { + fun execute(smtpDestination: SmtpDestination, message: MessageContent): DestinationMessageResponse { if (isMessageSizeOverLimit(message)) { return DestinationMessageResponse( RestStatus.REQUEST_ENTITY_TOO_LARGE.status, @@ -47,11 +47,11 @@ class DestinationEmailClient { val prop = Properties() prop["mail.transport.protocol"] = "smtp" - prop["mail.smtp.host"] = emailDestination.host - prop["mail.smtp.port"] = emailDestination.port + prop["mail.smtp.host"] = smtpDestination.host + prop["mail.smtp.port"] = smtpDestination.port val session = Session.getInstance(prop) - when (emailDestination.method) { + when (smtpDestination.method) { "ssl" -> prop["mail.smtp.ssl.enable"] = true "start_tls" -> prop["mail.smtp.starttls.enable"] = true "none" -> {} @@ -59,7 +59,7 @@ class DestinationEmailClient { } // prepare mimeMessage - val mimeMessage = EmailMimeProvider.prepareMimeMessage(session, emailDestination, message) + val mimeMessage = EmailMimeProvider.prepareMimeMessage(session, smtpDestination, message) // send Mime Message return sendMimeMessage(mimeMessage) diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/EmailMimeProvider.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/EmailMimeProvider.kt index fdc3141b..a2c5bea4 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/EmailMimeProvider.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/EmailMimeProvider.kt @@ -28,7 +28,7 @@ package org.opensearch.notifications.spi.client import org.opensearch.notifications.spi.model.MessageContent -import org.opensearch.notifications.spi.model.destination.EmailDestination +import org.opensearch.notifications.spi.model.destination.SmtpDestination import java.util.Base64 import javax.activation.DataHandler import javax.mail.Message @@ -45,23 +45,23 @@ internal object EmailMimeProvider { /** * Create and prepare mime mimeMessage to send mail * @param session The mail session to use to create mime mimeMessage - * @param emailDestination + * @param smtpDestination * @param mimeMessage The mimeMessage to send notification * @return The created and prepared mime mimeMessage object */ fun prepareMimeMessage( session: Session, - emailDestination: EmailDestination, + smtpDestination: SmtpDestination, messageContent: MessageContent ): MimeMessage { // Create a new MimeMessage object val mimeMessage = MimeMessage(session) // Add from: - mimeMessage.setFrom(emailDestination.fromAddress) + mimeMessage.setFrom(smtpDestination.fromAddress) // Add to: - mimeMessage.setRecipients(Message.RecipientType.TO, emailDestination.recipient) + mimeMessage.setRecipients(Message.RecipientType.TO, smtpDestination.recipient) // Add Subject: mimeMessage.setSubject(messageContent.title, "UTF-8") diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/factory/DestinationFactoryProvider.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/factory/DestinationFactoryProvider.kt deleted file mode 100644 index 0523c2b9..00000000 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/factory/DestinationFactoryProvider.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.notifications.spi.factory - -import org.opensearch.notifications.spi.model.destination.BaseDestination -import org.opensearch.notifications.spi.model.destination.DestinationType - -/** - * This class helps in fetching the right destination factory based on type - * A Destination could be Email, Webhook etc - */ -internal object DestinationFactoryProvider { - - var destinationFactoryMap = mapOf( - // TODO Add other destinations, ses, sns - DestinationType.SLACK to WebhookDestinationFactory(), - DestinationType.CHIME to WebhookDestinationFactory(), - DestinationType.CUSTOMWEBHOOK to WebhookDestinationFactory(), - DestinationType.SMTP to SmtpEmailDestinationFactory() - ) - - /** - * Fetches the right destination factory based on the type - * - * @param destinationType [{@link DestinationType}] - * @return DestinationFactory factory object for above destination type - */ - fun getFactory(destinationType: DestinationType): DestinationFactory { - require(destinationFactoryMap.containsKey(destinationType)) { "Invalid channel type" } - return destinationFactoryMap[destinationType] as DestinationFactory - } -} diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/CustomWebhookDestination.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/CustomWebhookDestination.kt index 90650f69..9b27fbfa 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/CustomWebhookDestination.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/CustomWebhookDestination.kt @@ -20,7 +20,7 @@ class CustomWebhookDestination( url: String, val headerParams: Map, val method: String -) : WebhookDestination(url, DestinationType.CUSTOMWEBHOOK) { +) : WebhookDestination(url, DestinationType.CUSTOM_WEBHOOK) { init { validateMethod(method) diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/DestinationType.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/DestinationType.kt index 99c89c9f..fa5ae7dc 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/DestinationType.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/DestinationType.kt @@ -14,5 +14,5 @@ package org.opensearch.notifications.spi.model.destination * Supported notification destinations */ enum class DestinationType { - CHIME, SLACK, CUSTOMWEBHOOK, SMTP, SES, SNS + CHIME, SLACK, CUSTOM_WEBHOOK, SMTP, SES, SNS } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/EmailDestination.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SmtpDestination.kt similarity index 67% rename from notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/EmailDestination.kt rename to notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SmtpDestination.kt index bf5fbf14..39526ce0 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/EmailDestination.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SmtpDestination.kt @@ -31,26 +31,20 @@ import org.opensearch.common.Strings import org.opensearch.notifications.spi.utils.validateEmail /** - * This class holds the contents of email destination + * This class holds the contents of smtp destination */ -class EmailDestination( +class SmtpDestination( val host: String, val port: Int, val method: String, val fromAddress: String, - val recipient: String, - destinationType: DestinationType, // smtp or ses -) : BaseDestination(destinationType) { + val recipient: String +) : BaseDestination(DestinationType.SMTP) { init { - when (destinationType) { - DestinationType.SMTP -> { - require(!Strings.isNullOrEmpty(host)) { "Host name should be provided" } - require(port > 0) { "Port should be positive value" } - validateEmail(fromAddress) - validateEmail(recipient) - } - // TODO Add ses here - } + require(!Strings.isNullOrEmpty(host)) { "Host name should be provided" } + require(port > 0) { "Port should be positive value" } + validateEmail(fromAddress) + validateEmail(recipient) } } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/factory/DestinationFactory.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransport.kt similarity index 89% rename from notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/factory/DestinationFactory.kt rename to notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransport.kt index a97e39fa..28598803 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/factory/DestinationFactory.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransport.kt @@ -9,7 +9,7 @@ * GitHub history for details. */ -package org.opensearch.notifications.spi.factory +package org.opensearch.notifications.spi.transport import org.opensearch.notifications.spi.model.DestinationMessageResponse import org.opensearch.notifications.spi.model.MessageContent @@ -20,7 +20,7 @@ import org.opensearch.notifications.spi.model.destination.BaseDestination * * @param message object of type [{@link DestinationType}] */ -internal interface DestinationFactory { +internal interface DestinationTransport { /** * Sending notification message over this channel. * diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransportProvider.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransportProvider.kt new file mode 100644 index 00000000..8e58824b --- /dev/null +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransportProvider.kt @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.spi.transport + +import org.opensearch.notifications.spi.model.destination.BaseDestination +import org.opensearch.notifications.spi.model.destination.DestinationType +import org.opensearch.notifications.spi.utils.OpenForTesting + +/** + * This class helps in fetching the right destination transport based on type + * A Destination could be SMTP, Webhook etc + */ +internal object DestinationTransportProvider { + + private val webhookDestinationTransport = WebhookDestinationTransport() + private val smtpDestinationTransport = SmtpDestinationTransport() + + @OpenForTesting + var destinationTransportMap = mapOf( + // TODO Add other destinations, ses, sns + DestinationType.SLACK to webhookDestinationTransport, + DestinationType.CHIME to webhookDestinationTransport, + DestinationType.CUSTOM_WEBHOOK to webhookDestinationTransport, + DestinationType.SMTP to smtpDestinationTransport + ) + + /** + * Fetches the right destination transport based on the type + * + * @param destinationType [{@link DestinationType}] + * @return DestinationTransport transport object for above destination type + */ + @Suppress("UNCHECKED_CAST") + fun getTransport(destinationType: DestinationType): DestinationTransport { + val retVal = destinationTransportMap[destinationType] ?: throw IllegalArgumentException("Invalid channel type") + return retVal as DestinationTransport + } +} diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/factory/SmtpEmailDestinationFactory.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SmtpDestinationTransport.kt similarity index 77% rename from notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/factory/SmtpEmailDestinationFactory.kt rename to notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SmtpDestinationTransport.kt index b0eec98d..813040a8 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/factory/SmtpEmailDestinationFactory.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SmtpDestinationTransport.kt @@ -9,13 +9,13 @@ * GitHub history for details. */ -package org.opensearch.notifications.spi.factory +package org.opensearch.notifications.spi.transport import org.opensearch.notifications.spi.client.DestinationClientPool -import org.opensearch.notifications.spi.client.DestinationEmailClient +import org.opensearch.notifications.spi.client.DestinationSmtpClient import org.opensearch.notifications.spi.model.DestinationMessageResponse import org.opensearch.notifications.spi.model.MessageContent -import org.opensearch.notifications.spi.model.destination.EmailDestination +import org.opensearch.notifications.spi.model.destination.SmtpDestination import org.opensearch.notifications.spi.utils.OpenForTesting import org.opensearch.notifications.spi.utils.logger import org.opensearch.rest.RestStatus @@ -26,21 +26,21 @@ import javax.mail.internet.AddressException /** * This class handles the client responsible for submitting the messages to all types of email destinations. */ -internal class SmtpEmailDestinationFactory : DestinationFactory { +internal class SmtpDestinationTransport : DestinationTransport { - private val log by logger(SmtpEmailDestinationFactory::class.java) - private val destinationEmailClient: DestinationEmailClient + private val log by logger(SmtpDestinationTransport::class.java) + private val destinationEmailClient: DestinationSmtpClient constructor() { - this.destinationEmailClient = DestinationClientPool.emailClient + this.destinationEmailClient = DestinationClientPool.smtpClient } @OpenForTesting - constructor(destinationEmailClient: DestinationEmailClient) { - this.destinationEmailClient = destinationEmailClient + constructor(destinationSmtpClient: DestinationSmtpClient) { + this.destinationEmailClient = destinationSmtpClient } - override fun sendMessage(destination: EmailDestination, message: MessageContent): DestinationMessageResponse { + override fun sendMessage(destination: SmtpDestination, message: MessageContent): DestinationMessageResponse { return try { destinationEmailClient.execute(destination, message) } catch (addressException: AddressException) { diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/factory/WebhookDestinationFactory.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/WebhookDestinationTransport.kt similarity index 89% rename from notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/factory/WebhookDestinationFactory.kt rename to notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/WebhookDestinationTransport.kt index 89a2077d..cdca28b0 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/factory/WebhookDestinationFactory.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/WebhookDestinationTransport.kt @@ -9,7 +9,7 @@ * GitHub history for details. */ -package org.opensearch.notifications.spi.factory +package org.opensearch.notifications.spi.transport import org.opensearch.notifications.spi.client.DestinationClientPool import org.opensearch.notifications.spi.client.DestinationHttpClient @@ -24,9 +24,9 @@ import java.io.IOException /** * This class handles the client responsible for submitting the messages to all types of webhook destinations. */ -internal class WebhookDestinationFactory : DestinationFactory { +internal class WebhookDestinationTransport : DestinationTransport { - private val log by logger(WebhookDestinationFactory::class.java) + private val log by logger(WebhookDestinationTransport::class.java) private val destinationHttpClient: DestinationHttpClient constructor() { diff --git a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/ChimeDestinationTests.kt b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/ChimeDestinationTests.kt index 7c1d0dc7..b3fd82fb 100644 --- a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/ChimeDestinationTests.kt +++ b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/ChimeDestinationTests.kt @@ -41,12 +41,12 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import org.opensearch.notifications.spi.client.DestinationHttpClient -import org.opensearch.notifications.spi.factory.DestinationFactoryProvider -import org.opensearch.notifications.spi.factory.WebhookDestinationFactory import org.opensearch.notifications.spi.model.DestinationMessageResponse import org.opensearch.notifications.spi.model.MessageContent import org.opensearch.notifications.spi.model.destination.ChimeDestination import org.opensearch.notifications.spi.model.destination.DestinationType +import org.opensearch.notifications.spi.transport.DestinationTransportProvider +import org.opensearch.notifications.spi.transport.WebhookDestinationTransport import org.opensearch.rest.RestStatus import java.net.MalformedURLException import java.util.stream.Stream @@ -83,8 +83,8 @@ internal class ChimeDestinationTests { EasyMock.replay(mockStatusLine) val httpClient = DestinationHttpClient(mockHttpClient) - val webhookDestinationFactory = WebhookDestinationFactory(httpClient) - DestinationFactoryProvider.destinationFactoryMap = mapOf(DestinationType.CHIME to webhookDestinationFactory) + val webhookDestinationTransport = WebhookDestinationTransport(httpClient) + DestinationTransportProvider.destinationTransportMap = mapOf(DestinationType.CHIME to webhookDestinationTransport) val title = "test Chime" val messageText = "Message gughjhjlkh Body emoji test: :) :+1: " + @@ -117,8 +117,8 @@ internal class ChimeDestinationTests { EasyMock.replay(mockStatusLine) val httpClient = DestinationHttpClient(mockHttpClient) - val webhookDestinationFactory = WebhookDestinationFactory(httpClient) - DestinationFactoryProvider.destinationFactoryMap = mapOf(DestinationType.CHIME to webhookDestinationFactory) + val webhookDestinationTransport = WebhookDestinationTransport(httpClient) + DestinationTransportProvider.destinationTransportMap = mapOf(DestinationType.CHIME to webhookDestinationTransport) val title = "test Chime" val messageText = "{\"Content\":\"Message gughjhjlkh Body emoji test: :) :+1: " + @@ -152,8 +152,8 @@ internal class ChimeDestinationTests { EasyMock.replay(mockStatusLine) val httpClient = DestinationHttpClient(mockHttpClient) - val webhookDestinationFactory = WebhookDestinationFactory(httpClient) - DestinationFactoryProvider.destinationFactoryMap = mapOf(DestinationType.CHIME to webhookDestinationFactory) + val webhookDestinationTransport = WebhookDestinationTransport(httpClient) + DestinationTransportProvider.destinationTransportMap = mapOf(DestinationType.CHIME to webhookDestinationTransport) val title = "test Chime" val messageText = "{\"Content\":\"Message gughjhjlkh Body emoji test: :) :+1: " + diff --git a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/CustomWebhookDestinationTests.kt b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/CustomWebhookDestinationTests.kt index e2b6c952..58a1bb46 100644 --- a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/CustomWebhookDestinationTests.kt +++ b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/CustomWebhookDestinationTests.kt @@ -44,12 +44,12 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import org.opensearch.notifications.spi.client.DestinationHttpClient -import org.opensearch.notifications.spi.factory.DestinationFactoryProvider -import org.opensearch.notifications.spi.factory.WebhookDestinationFactory import org.opensearch.notifications.spi.model.DestinationMessageResponse import org.opensearch.notifications.spi.model.MessageContent import org.opensearch.notifications.spi.model.destination.CustomWebhookDestination import org.opensearch.notifications.spi.model.destination.DestinationType +import org.opensearch.notifications.spi.transport.DestinationTransportProvider +import org.opensearch.notifications.spi.transport.WebhookDestinationTransport import org.opensearch.rest.RestStatus import java.net.MalformedURLException import java.util.stream.Stream @@ -95,9 +95,9 @@ internal class CustomWebhookDestinationTests { EasyMock.replay(mockStatusLine) val httpClient = DestinationHttpClient(mockHttpClient) - val webhookDestinationFactory = WebhookDestinationFactory(httpClient) - DestinationFactoryProvider.destinationFactoryMap = mapOf( - DestinationType.CUSTOMWEBHOOK to webhookDestinationFactory + val webhookDestinationTransport = WebhookDestinationTransport(httpClient) + DestinationTransportProvider.destinationTransportMap = mapOf( + DestinationType.CUSTOM_WEBHOOK to webhookDestinationTransport ) val title = "test custom webhook" @@ -134,9 +134,9 @@ internal class CustomWebhookDestinationTests { EasyMock.replay(mockStatusLine) val httpClient = DestinationHttpClient(mockHttpClient) - val webhookDestinationFactory = WebhookDestinationFactory(httpClient) - DestinationFactoryProvider.destinationFactoryMap = mapOf( - DestinationType.CUSTOMWEBHOOK to webhookDestinationFactory + val webhookDestinationTransport = WebhookDestinationTransport(httpClient) + DestinationTransportProvider.destinationTransportMap = mapOf( + DestinationType.CUSTOM_WEBHOOK to webhookDestinationTransport ) val title = "test custom webhook" @@ -176,9 +176,9 @@ internal class CustomWebhookDestinationTests { EasyMock.replay(mockStatusLine) val httpClient = DestinationHttpClient(mockHttpClient) - val webhookDestinationFactory = WebhookDestinationFactory(httpClient) - DestinationFactoryProvider.destinationFactoryMap = mapOf( - DestinationType.CUSTOMWEBHOOK to webhookDestinationFactory + val webhookDestinationTransport = WebhookDestinationTransport(httpClient) + DestinationTransportProvider.destinationTransportMap = mapOf( + DestinationType.CUSTOM_WEBHOOK to webhookDestinationTransport ) val title = "test custom webhook" diff --git a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/SlackDestinationTests.kt b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/SlackDestinationTests.kt index e0a88a4c..530f3800 100644 --- a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/SlackDestinationTests.kt +++ b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/SlackDestinationTests.kt @@ -41,13 +41,13 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import org.opensearch.notifications.spi.client.DestinationHttpClient -import org.opensearch.notifications.spi.factory.DestinationFactoryProvider -import org.opensearch.notifications.spi.factory.WebhookDestinationFactory import org.opensearch.notifications.spi.model.DestinationMessageResponse import org.opensearch.notifications.spi.model.MessageContent import org.opensearch.notifications.spi.model.destination.ChimeDestination import org.opensearch.notifications.spi.model.destination.DestinationType import org.opensearch.notifications.spi.model.destination.SlackDestination +import org.opensearch.notifications.spi.transport.DestinationTransportProvider +import org.opensearch.notifications.spi.transport.WebhookDestinationTransport import org.opensearch.rest.RestStatus import java.net.MalformedURLException import java.util.stream.Stream @@ -84,8 +84,8 @@ internal class SlackDestinationTests { EasyMock.replay(mockStatusLine) val httpClient = DestinationHttpClient(mockHttpClient) - val webhookDestinationFactory = WebhookDestinationFactory(httpClient) - DestinationFactoryProvider.destinationFactoryMap = mapOf(DestinationType.SLACK to webhookDestinationFactory) + val webhookDestinationTransport = WebhookDestinationTransport(httpClient) + DestinationTransportProvider.destinationTransportMap = mapOf(DestinationType.SLACK to webhookDestinationTransport) val title = "test Slack" val messageText = "Message gughjhjlkh Body emoji test: :) :+1: " + @@ -118,8 +118,8 @@ internal class SlackDestinationTests { EasyMock.replay(mockStatusLine) val httpClient = DestinationHttpClient(mockHttpClient) - val webhookDestinationFactory = WebhookDestinationFactory(httpClient) - DestinationFactoryProvider.destinationFactoryMap = mapOf(DestinationType.SLACK to webhookDestinationFactory) + val webhookDestinationTransport = WebhookDestinationTransport(httpClient) + DestinationTransportProvider.destinationTransportMap = mapOf(DestinationType.SLACK to webhookDestinationTransport) val title = "test Slack" val messageText = "{\"Content\":\"Message gughjhjlkh Body emoji test: :) :+1: " + @@ -153,8 +153,8 @@ internal class SlackDestinationTests { EasyMock.replay(mockStatusLine) val httpClient = DestinationHttpClient(mockHttpClient) - val webhookDestinationFactory = WebhookDestinationFactory(httpClient) - DestinationFactoryProvider.destinationFactoryMap = mapOf(DestinationType.SLACK to webhookDestinationFactory) + val webhookDestinationTransport = WebhookDestinationTransport(httpClient) + DestinationTransportProvider.destinationTransportMap = mapOf(DestinationType.SLACK to webhookDestinationTransport) val title = "test Slack" val messageText = "{\"Content\":\"Message gughjhjlkh Body emoji test: :) :+1: " + diff --git a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/EmailDestinationTests.kt b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/SmtpDestinationTests.kt similarity index 73% rename from notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/EmailDestinationTests.kt rename to notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/SmtpDestinationTests.kt index 8e5f252f..8d0ac9ae 100644 --- a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/EmailDestinationTests.kt +++ b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/SmtpDestinationTests.kt @@ -18,35 +18,35 @@ import org.junit.Assert import org.junit.Test import org.junit.jupiter.api.extension.ExtendWith import org.mockito.junit.jupiter.MockitoExtension -import org.opensearch.notifications.spi.client.DestinationEmailClient -import org.opensearch.notifications.spi.factory.DestinationFactoryProvider -import org.opensearch.notifications.spi.factory.SmtpEmailDestinationFactory +import org.opensearch.notifications.spi.client.DestinationSmtpClient import org.opensearch.notifications.spi.model.DestinationMessageResponse import org.opensearch.notifications.spi.model.MessageContent import org.opensearch.notifications.spi.model.destination.DestinationType -import org.opensearch.notifications.spi.model.destination.EmailDestination +import org.opensearch.notifications.spi.model.destination.SmtpDestination +import org.opensearch.notifications.spi.transport.DestinationTransportProvider +import org.opensearch.notifications.spi.transport.SmtpDestinationTransport import org.opensearch.rest.RestStatus import javax.mail.MessagingException @ExtendWith(MockitoExtension::class) -internal class EmailDestinationTests { +internal class SmtpDestinationTests { @Test @Throws(Exception::class) fun testSmtpEmailMessage() { val expectedEmailResponse = DestinationMessageResponse(RestStatus.OK.status, "Success") - val emailClient = spyk() + val emailClient = spyk() every { emailClient.sendMessage(any()) } returns Unit - val smtpEmailDestinationFactory = SmtpEmailDestinationFactory(emailClient) - DestinationFactoryProvider.destinationFactoryMap = mapOf(DestinationType.SMTP to smtpEmailDestinationFactory) + val smtpEmailDestinationTransport = SmtpDestinationTransport(emailClient) + DestinationTransportProvider.destinationTransportMap = mapOf(DestinationType.SMTP to smtpEmailDestinationTransport) val subject = "Test SMTP Email subject" val messageText = "{Message gughjhjlkh Body emoji test: :) :+1: " + "link test: http://sample.com email test: marymajor@example.com All member callout: " + "@All All Present member callout: @Present}" val message = MessageContent(subject, messageText) - val destination = EmailDestination("abc", 465, "ssl", "test@abc.com", "to@abc.com", DestinationType.SMTP) + val destination = SmtpDestination("abc", 465, "ssl", "test@abc.com", "to@abc.com") val actualEmailResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message) assertEquals(expectedEmailResponse.statusCode, actualEmailResponse.statusCode) @@ -60,26 +60,25 @@ internal class EmailDestinationTests { RestStatus.FAILED_DEPENDENCY.status, "Couldn't connect to host, port: localhost, 55555; timeout -1" ) - val emailClient = spyk() + val emailClient = spyk() every { emailClient.sendMessage(any()) } throws MessagingException( "Couldn't connect to host, port: localhost, 55555; timeout -1" ) - val smtpEmailDestinationFactory = SmtpEmailDestinationFactory(emailClient) - DestinationFactoryProvider.destinationFactoryMap = mapOf(DestinationType.SMTP to smtpEmailDestinationFactory) + val smtpEmailDestinationTransport = SmtpDestinationTransport(emailClient) + DestinationTransportProvider.destinationTransportMap = mapOf(DestinationType.SMTP to smtpEmailDestinationTransport) val subject = "Test SMTP Email subject" val messageText = "{Vamshi Message gughjhjlkh Body emoji test: :) :+1: " + "link test: http://sample.com email test: marymajor@example.com All member callout: " + "@All All Present member callout: @Present}" val message = MessageContent(subject, messageText) - val destination = EmailDestination( + val destination = SmtpDestination( "localhost", 55555, "none", "test@abc.com", - "to@abc.com", - DestinationType.SMTP + "to@abc.com" ) val actualEmailResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message) @@ -91,7 +90,7 @@ internal class EmailDestinationTests { @Test(expected = IllegalArgumentException::class) fun testHostMissingEmailDestination() { try { - EmailDestination("", 465, "ssl", "from@test.com", "to@test.com", DestinationType.SMTP) + SmtpDestination("", 465, "ssl", "from@test.com", "to@test.com") } catch (exception: Exception) { Assert.assertEquals("Host name should be provided", exception.message) throw exception @@ -101,7 +100,7 @@ internal class EmailDestinationTests { @Test(expected = IllegalArgumentException::class) fun testInvalidPortEmailDestination() { try { - EmailDestination("localhost", -1, "ssl", "from@test.com", "to@test.com", DestinationType.SMTP) + SmtpDestination("localhost", -1, "ssl", "from@test.com", "to@test.com") } catch (exception: Exception) { Assert.assertEquals("Port should be positive value", exception.message) throw exception @@ -111,7 +110,7 @@ internal class EmailDestinationTests { @Test(expected = IllegalArgumentException::class) fun testMissingFromOrRecipientEmailDestination() { try { - EmailDestination("localhost", 465, "ssl", "", "to@test.com", DestinationType.SMTP) + SmtpDestination("localhost", 465, "ssl", "", "to@test.com") } catch (exception: Exception) { Assert.assertEquals("FromAddress and recipient should be provided", exception.message) throw exception diff --git a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/integTest/SmtpEmailIT.kt b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/integTest/SmtpEmailIT.kt index 4fa77e40..a4b9064a 100644 --- a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/integTest/SmtpEmailIT.kt +++ b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/integTest/SmtpEmailIT.kt @@ -14,8 +14,7 @@ package org.opensearch.notifications.spi.integTest import org.junit.After import org.opensearch.notifications.spi.NotificationSpi import org.opensearch.notifications.spi.model.MessageContent -import org.opensearch.notifications.spi.model.destination.DestinationType -import org.opensearch.notifications.spi.model.destination.EmailDestination +import org.opensearch.notifications.spi.model.destination.SmtpDestination import org.opensearch.rest.RestStatus import org.opensearch.test.rest.OpenSearchRestTestCase import org.springframework.integration.test.mail.TestMailServer @@ -36,13 +35,12 @@ internal class SmtpEmailIT : OpenSearchRestTestCase() { } fun `test send email to one recipient over smtp server`() { - val emailDestination = EmailDestination( + val smtpDestination = SmtpDestination( "localhost", smtpPort, "none", "from@email.com", - "test@localhost.com", - DestinationType.SMTP + "test@localhost.com" ) val message = MessageContent( "Test smtp email title", @@ -53,19 +51,18 @@ internal class SmtpEmailIT : OpenSearchRestTestCase() { "VGVzdCBtZXNzYWdlCgo=", "application/octet-stream", ) - val response = NotificationSpi.sendMessage(emailDestination, message) + val response = NotificationSpi.sendMessage(smtpDestination, message) assertEquals("Success", response.statusText) assertEquals(RestStatus.OK.status, response.statusCode) } fun `test send email with non-available host`() { - val emailDestination = EmailDestination( + val smtpDestination = SmtpDestination( "invalidHost", smtpPort, "none", "from@email.com", - "test@localhost.com", - DestinationType.SMTP + "test@localhost.com" ) val message = MessageContent( "Test smtp email title", @@ -76,7 +73,7 @@ internal class SmtpEmailIT : OpenSearchRestTestCase() { "VGVzdCBtZXNzYWdlCgo=", "application/octet-stream", ) - val response = NotificationSpi.sendMessage(emailDestination, message) + val response = NotificationSpi.sendMessage(smtpDestination, message) assertEquals( "sendEmail Error, status:Couldn't connect to host, port: invalidHost, $smtpPort; timeout -1", response.statusText From 2f46c8c9ca159efb839a73763717f5811ec393a5 Mon Sep 17 00:00:00 2001 From: Joshua Date: Thu, 5 Aug 2021 13:01:17 -0700 Subject: [PATCH 08/29] Frontend: Add parsing logic for SNS channels (#249) --- dashboards-notifications/models/interfaces.ts | 4 ++ .../DetailsListModal.test.tsx.snap | 2 +- .../DetailsTableModal.test.tsx.snap | 4 +- .../details/ChannelSettingsDetails.tsx | 45 +++++++------------ .../pages/CreateChannel/CreateChannel.tsx | 15 ++++--- .../__tests__/validationHelper.test.ts | 8 +++- .../components/EmailSettings.tsx | 8 ++++ .../CreateChannel/components/SNSSettings.tsx | 10 ++--- .../CreateChannel/utils/validationHelper.ts | 3 +- .../public/pages/Main/Main.tsx | 11 +++-- .../public/services/NotificationService.ts | 23 +++++++--- .../server/clusters/notificationsPlugin.ts | 2 +- .../server/routes/configRoutes.ts | 2 +- .../test/mocks/serviceMock.ts | 1 + 14 files changed, 80 insertions(+), 58 deletions(-) diff --git a/dashboards-notifications/models/interfaces.ts b/dashboards-notifications/models/interfaces.ts index c78f1539..2a057bd1 100644 --- a/dashboards-notifications/models/interfaces.ts +++ b/dashboards-notifications/models/interfaces.ts @@ -88,6 +88,10 @@ export interface ChannelItemType extends ConfigType { [id: string]: string; }; }; + sns?: { + topic_arn: string; + role_arn?: string; + } } interface ConfigType { diff --git a/dashboards-notifications/public/pages/Channels/__tests__/__snapshots__/DetailsListModal.test.tsx.snap b/dashboards-notifications/public/pages/Channels/__tests__/__snapshots__/DetailsListModal.test.tsx.snap index d26df11d..e20292c3 100644 --- a/dashboards-notifications/public/pages/Channels/__tests__/__snapshots__/DetailsListModal.test.tsx.snap +++ b/dashboards-notifications/public/pages/Channels/__tests__/__snapshots__/DetailsListModal.test.tsx.snap @@ -28,7 +28,6 @@ exports[` spec renders the component 1`] = ` "notificationService": NotificationService { "createConfig": [Function], "deleteConfigs": [Function], - "getAvailableFeatures": [Function], "getChannel": [Function], "getChannels": [Function], "getConfig": [Function], @@ -38,6 +37,7 @@ exports[` spec renders the component 1`] = ` "getRecipientGroups": [Function], "getSender": [Function], "getSenders": [Function], + "getServerFeatures": [Function], "httpClient": [MockFunction], "updateConfig": [Function], }, diff --git a/dashboards-notifications/public/pages/Channels/__tests__/__snapshots__/DetailsTableModal.test.tsx.snap b/dashboards-notifications/public/pages/Channels/__tests__/__snapshots__/DetailsTableModal.test.tsx.snap index a7c19c56..41c4c804 100644 --- a/dashboards-notifications/public/pages/Channels/__tests__/__snapshots__/DetailsTableModal.test.tsx.snap +++ b/dashboards-notifications/public/pages/Channels/__tests__/__snapshots__/DetailsTableModal.test.tsx.snap @@ -29,7 +29,6 @@ exports[` spec renders headers 1`] = ` "notificationService": NotificationService { "createConfig": [Function], "deleteConfigs": [Function], - "getAvailableFeatures": [Function], "getChannel": [Function], "getChannels": [Function], "getConfig": [Function], @@ -39,6 +38,7 @@ exports[` spec renders headers 1`] = ` "getRecipientGroups": [Function], "getSender": [Function], "getSenders": [Function], + "getServerFeatures": [Function], "httpClient": [MockFunction], "updateConfig": [Function], }, @@ -1862,7 +1862,6 @@ exports[` spec renders parameters 1`] = ` "notificationService": NotificationService { "createConfig": [Function], "deleteConfigs": [Function], - "getAvailableFeatures": [Function], "getChannel": [Function], "getChannels": [Function], "getConfig": [Function], @@ -1872,6 +1871,7 @@ exports[` spec renders parameters 1`] = ` "getRecipientGroups": [Function], "getSender": [Function], "getSenders": [Function], + "getServerFeatures": [Function], "httpClient": [MockFunction], "updateConfig": [Function], }, diff --git a/dashboards-notifications/public/pages/Channels/components/details/ChannelSettingsDetails.tsx b/dashboards-notifications/public/pages/Channels/components/details/ChannelSettingsDetails.tsx index 81c6bd9f..a89ff49a 100644 --- a/dashboards-notifications/public/pages/Channels/components/details/ChannelSettingsDetails.tsx +++ b/dashboards-notifications/public/pages/Channels/components/details/ChannelSettingsDetails.tsx @@ -143,19 +143,6 @@ export function ChannelSettingsDetails(props: ChannelSettingsDetailsProps) { title: 'Default recipients', description: recipientsDescription, }, - // TODO remove when removing header/footer functionality - // { - // title: 'Email header', - // description: props.channel.destination.email.header - // ? 'Enabled' - // : 'Disabled', - // }, - // { - // title: 'Email footer', - // description: props.channel.destination.email.footer - // ? 'Enabled' - // : 'Disabled', - // }, ] ); } else if (type === BACKEND_CHANNEL_TYPE.CUSTOM_WEBHOOK) { @@ -203,22 +190,22 @@ export function ChannelSettingsDetails(props: ChannelSettingsDetailsProps) { ] ); } else if (type === BACKEND_CHANNEL_TYPE.SNS) { - // settingsList.push( - // ...[ - // { - // title: 'Channel type', - // description: CHANNEL_TYPE.SNS, - // }, - // { - // title: 'SNS topic ARN', - // description: props.channel.destination.sns.topic_arn || '-', - // }, - // { - // title: 'IAM role ARN', - // description: props.channel.destination.sns.role_arn || '-', - // }, - // ] - // ); + settingsList.push( + ...[ + { + title: 'Channel type', + description: CHANNEL_TYPE.sns, + }, + { + title: 'SNS topic ARN', + description: props.channel.sns?.topic_arn || '-', + }, + { + title: 'IAM role ARN', + description: props.channel.sns?.role_arn || '-', + }, + ] + ); } else if (type === BACKEND_CHANNEL_TYPE.SES) { // TODO } diff --git a/dashboards-notifications/public/pages/CreateChannel/CreateChannel.tsx b/dashboards-notifications/public/pages/CreateChannel/CreateChannel.tsx index 7130e1e6..d39c6027 100644 --- a/dashboards-notifications/public/pages/CreateChannel/CreateChannel.tsx +++ b/dashboards-notifications/public/pages/CreateChannel/CreateChannel.tsx @@ -90,8 +90,6 @@ export const CreateChannelContext = createContext<{ } | null>(null); export function CreateChannel(props: CreateChannelsProps) { - const isOdfe = true; - const coreContext = useContext(CoreServicesContext)!; const servicesContext = useContext(ServicesContext)!; const mainStateContext = useContext(MainContext)!; @@ -226,7 +224,8 @@ export function CreateChannel(props: CreateChannelsProps) { } else if (type === BACKEND_CHANNEL_TYPE.SES) { // TODO } else if (type === BACKEND_CHANNEL_TYPE.SNS) { - // TODO + setTopicArn(response.sns?.topic_arn || ''); + setRoleArn(response.sns?.role_arn || ''); } } catch (error) { coreContext.notifications.toasts.addDanger( @@ -265,7 +264,7 @@ export function CreateChannel(props: CreateChannelsProps) { } } else if (channelType === BACKEND_CHANNEL_TYPE.SNS) { errors.topicArn = validateArn(topicArn); - if (!isOdfe) errors.roleArn = validateArn(roleArn); + if (!mainStateContext.tooltipSupport) errors.roleArn = validateArn(roleArn); } setInputErrors(errors); return !Object.values(errors).reduce( @@ -303,6 +302,13 @@ export function CreateChannel(props: CreateChannelsProps) { selectedSenderOptions, selectedRecipientGroupOptions ); + } else if (channelType === BACKEND_CHANNEL_TYPE.SES) { + // TODO + } else if (channelType === BACKEND_CHANNEL_TYPE.SNS) { + config.sns = { + topic_arn: topicArn, + ...(roleArn && { role_arn: roleArn }), + }; } return config; }; @@ -450,7 +456,6 @@ export function CreateChannel(props: CreateChannelsProps) { /> ) : channelType === BACKEND_CHANNEL_TYPE.SNS ? ( { it('validates webhook', () => { const pass = validateWebhookURL('https://test-webhook'); + const httpTest = validateWebhookURL('http://test-webhook'); const emptyInput = validateWebhookURL(''); const invalidURL = validateWebhookURL('hxxp://test-webhook'); expect(pass).toHaveLength(0); + expect(httpTest).toHaveLength(0); expect(emptyInput).toHaveLength(1); expect(invalidURL).toHaveLength(1); }); @@ -69,11 +71,13 @@ describe('test create channel validation helpers', () => { it('validates custom url host', () => { const pass = validateCustomURLHost('test-webhook'); + const httpTest = validateCustomURLHost('http://test-webhook'); + const httpsTest = validateCustomURLHost('https://test-webhook'); const emptyInput = validateCustomURLHost(''); - const invalidURL = validateCustomURLHost('http://test-webhook'); // only https is allowed expect(pass).toHaveLength(0); + expect(httpTest).toHaveLength(0); + expect(httpsTest).toHaveLength(0); expect(emptyInput).toHaveLength(1); - expect(invalidURL).toHaveLength(1); }); it('validates custom url port', () => { diff --git a/dashboards-notifications/public/pages/CreateChannel/components/EmailSettings.tsx b/dashboards-notifications/public/pages/CreateChannel/components/EmailSettings.tsx index e66e0d97..d5a63886 100644 --- a/dashboards-notifications/public/pages/CreateChannel/components/EmailSettings.tsx +++ b/dashboards-notifications/public/pages/CreateChannel/components/EmailSettings.tsx @@ -212,6 +212,10 @@ export function EmailSettings(props: EmailSettingsProps) { ) => { setSenderOptions([...senderOptions, newOption]); props.setSelectedSenderOptions([newOption]); + context.setInputErrors({ + ...context.inputErrors, + sender: validateEmailSender([newOption]), + }); }, }) } @@ -273,6 +277,10 @@ export function EmailSettings(props: EmailSettingsProps) { ...props.selectedRecipientGroupOptions, newOption, ]); + context.setInputErrors({ + ...context.inputErrors, + recipients: validateRecipients([newOption]), + }); }, }) } diff --git a/dashboards-notifications/public/pages/CreateChannel/components/SNSSettings.tsx b/dashboards-notifications/public/pages/CreateChannel/components/SNSSettings.tsx index 1c542bed..c9726b05 100644 --- a/dashboards-notifications/public/pages/CreateChannel/components/SNSSettings.tsx +++ b/dashboards-notifications/public/pages/CreateChannel/components/SNSSettings.tsx @@ -34,11 +34,11 @@ import { } from '@elastic/eui'; import React, { useContext } from 'react'; import { DOCUMENTATION_LINK } from '../../../utils/constants'; +import { MainContext } from '../../Main/Main'; import { CreateChannelContext } from '../CreateChannel'; import { validateArn } from '../utils/validationHelper'; interface SNSSettingsProps { - isOdfe: boolean; topicArn: string; setTopicArn: (topicArn: string) => void; roleArn: string; @@ -47,6 +47,7 @@ interface SNSSettingsProps { export function SNSSettings(props: SNSSettingsProps) { const context = useContext(CreateChannelContext)!; + const mainStateContext = useContext(MainContext)!; return ( <> @@ -69,7 +70,7 @@ export function SNSSettings(props: SNSSettingsProps) { /> - {props.isOdfe ? ( + {mainStateContext.tooltipSupport ? ( <>
- If your cluster is not running on AWS, you must add your access - key, secret key, and optional session token to the OpenSearch - keystore.{' '} + If your cluster is not running on AWS, you must configure aws + credentials on your OpenSearch cluster.{' '} Learn more diff --git a/dashboards-notifications/public/pages/CreateChannel/utils/validationHelper.ts b/dashboards-notifications/public/pages/CreateChannel/utils/validationHelper.ts index 2b6fb5f9..e10c4570 100644 --- a/dashboards-notifications/public/pages/CreateChannel/utils/validationHelper.ts +++ b/dashboards-notifications/public/pages/CreateChannel/utils/validationHelper.ts @@ -36,7 +36,7 @@ export const validateChannelName = (name: string) => { export const validateWebhookURL = (url: string) => { const errors = []; if (url.length === 0) errors.push('Webhook URL cannot be empty.'); - else if (!url.match(/^https:\/\/.+/)) errors.push('Invalid webhook URL.'); + else if (!url.match(/^https?:\/\/.+/)) errors.push('Invalid webhook URL.'); return errors; }; @@ -55,7 +55,6 @@ export const validateWebhookValue = (value: string) => { export const validateCustomURLHost = (host: string) => { const errors = []; if (host.length === 0) errors.push('Host cannot be empty.'); - else if (host.match(/^http:\/\//)) errors.push('Invalid webhook URL.'); return errors; }; diff --git a/dashboards-notifications/public/pages/Main/Main.tsx b/dashboards-notifications/public/pages/Main/Main.tsx index 885de089..fe801405 100644 --- a/dashboards-notifications/public/pages/Main/Main.tsx +++ b/dashboards-notifications/public/pages/Main/Main.tsx @@ -57,6 +57,7 @@ interface MainProps extends RouteComponentProps {} export interface MainState { availableFeatures: Partial; + tooltipSupport: boolean; // if true, IAM role for SNS is optional and helper text should be available } export const MainContext = createContext(null); @@ -68,13 +69,17 @@ export default class Main extends Component { super(props); this.state = { availableFeatures: CHANNEL_TYPE, + tooltipSupport: false, }; } async componentDidMount() { - const availableFeatures = - await this.context.notificationService.getAvailableFeatures(); - if (availableFeatures != null) this.setState({ availableFeatures }); + const serverFeatures = await this.context.notificationService.getServerFeatures(); + if (serverFeatures != null) + this.setState({ + availableFeatures: serverFeatures.availableFeatures, + tooltipSupport: serverFeatures.tooltipSupport, + }); } render() { diff --git a/dashboards-notifications/public/services/NotificationService.ts b/dashboards-notifications/public/services/NotificationService.ts index 8a4755dd..c0711089 100644 --- a/dashboards-notifications/public/services/NotificationService.ts +++ b/dashboards-notifications/public/services/NotificationService.ts @@ -25,6 +25,7 @@ */ import { SortDirection } from '@elastic/eui'; +import _ from 'lodash'; import { HttpFetchQuery, HttpSetup } from '../../../../src/core/public'; import { NODE_API } from '../../common'; import { @@ -167,20 +168,28 @@ export default class NotificationService { return configToRecipientGroup(response.config_list[0]); }; - getAvailableFeatures = async () => { + getServerFeatures = async () => { try { - const channels = (await this.httpClient - .get(NODE_API.GET_AVAILABLE_FEATURES) - .then((response) => response.config_type_list)) as Array< + const response = await this.httpClient.get( + NODE_API.GET_AVAILABLE_FEATURES + ); + const config_type_list = response.config_type_list as Array< keyof typeof CHANNEL_TYPE >; const channelTypes: Partial = {}; - for (let i = 0; i < channels.length; i++) { - const channel = channels[i]; + for (let i = 0; i < config_type_list.length; i++) { + const channel = config_type_list[i]; if (!CHANNEL_TYPE[channel]) continue; channelTypes[channel] = CHANNEL_TYPE[channel]; } - return channelTypes; + return { + availableFeatures: channelTypes, + tooltipSupport: + _.get(response, [ + 'plugin_features', + 'opensearch.notifications.spi.tooltip_support', + ]) === 'true', + }; } catch (error) { console.error('error fetching available features', error); return null; diff --git a/dashboards-notifications/server/clusters/notificationsPlugin.ts b/dashboards-notifications/server/clusters/notificationsPlugin.ts index 6c29702b..1b8446f8 100644 --- a/dashboards-notifications/server/clusters/notificationsPlugin.ts +++ b/dashboards-notifications/server/clusters/notificationsPlugin.ts @@ -120,7 +120,7 @@ export function NotificationsPlugin(Client: any, config: any, components: any) { method: 'GET', }); - notifications.getAvailableFeatures = clientAction({ + notifications.getServerFeatures = clientAction({ url: { fmt: OPENSEARCH_API.FEATURES, }, diff --git a/dashboards-notifications/server/routes/configRoutes.ts b/dashboards-notifications/server/routes/configRoutes.ts index b786ec02..8379e18e 100644 --- a/dashboards-notifications/server/routes/configRoutes.ts +++ b/dashboards-notifications/server/routes/configRoutes.ts @@ -210,7 +210,7 @@ export function configRoutes(router: IRouter) { ); try { const resp = await client.callAsCurrentUser( - 'notifications.getAvailableFeatures' + 'notifications.getServerFeatures' ); return response.ok({ body: resp }); } catch (error) { diff --git a/dashboards-notifications/test/mocks/serviceMock.ts b/dashboards-notifications/test/mocks/serviceMock.ts index cef25647..da6c9ecc 100644 --- a/dashboards-notifications/test/mocks/serviceMock.ts +++ b/dashboards-notifications/test/mocks/serviceMock.ts @@ -55,6 +55,7 @@ const notificationServiceMock = { const mainStateMock: MainState = { availableFeatures: CHANNEL_TYPE, + tooltipSupport: true, }; export { notificationServiceMock, coreServicesMock, mainStateMock }; From 2395c672f4cd3074fbd2d42a02bff2cbe1f904f9 Mon Sep 17 00:00:00 2001 From: Zhongnan Su Date: Fri, 6 Aug 2021 13:57:17 -0700 Subject: [PATCH 09/29] Bump opensearch-notification version to 1.1 (#264) * exclude test to pass CI * bump version for notification-opensearch to 1.1 --- .../notifications-test-and-build-workflow.yml | 10 ++++++---- notifications/build.gradle | 2 +- notifications/gradle.properties | 2 +- notifications/notifications/build.gradle | 5 +++++ 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/notifications-test-and-build-workflow.yml b/.github/workflows/notifications-test-and-build-workflow.yml index 15799e94..f1c3173f 100644 --- a/.github/workflows/notifications-test-and-build-workflow.yml +++ b/.github/workflows/notifications-test-and-build-workflow.yml @@ -26,8 +26,9 @@ name: Test and Build Notifications on: [push, pull_request] env: - OPENSEARCH_VERSION: '1.0' - COMMON_UTILS_VERSION: 'main' + OPENSEARCH_VERSION: '1.1' + COMMON_UTILS_BRANCH: 'main' + OPENSEARCH_BRANCH: 'main' jobs: build: @@ -45,7 +46,7 @@ jobs: with: repository: 'opensearch-project/OpenSearch' path: OpenSearch - ref: ${{ env.OPENSEARCH_VERSION }} + ref: ${{ env.OPENSEARCH_BRANCH }} - name: Build OpenSearch working-directory: ./OpenSearch run: ./gradlew publishToMavenLocal -Dbuild.snapshot=false @@ -65,10 +66,11 @@ jobs: - name: Checkout Notifications uses: actions/checkout@v2 + # Temporarily exclude tests which causing CI to fail. Tracking in #251 - name: Build with Gradle run: | cd notifications - ./gradlew build -PexcludeTests="**/SesChannelIT*" -Dopensearch.version=${{ env.OPENSEARCH_VERSION }}.0 + ./gradlew build -PexcludeTests="**/SesChannelIT*, **/PluginActionTests*" -Dopensearch.version=${{ env.OPENSEARCH_VERSION }}.0 - name: Upload coverage uses: codecov/codecov-action@v1 diff --git a/notifications/build.gradle b/notifications/build.gradle index f0979a7f..227030e9 100644 --- a/notifications/build.gradle +++ b/notifications/build.gradle @@ -28,7 +28,7 @@ buildscript { ext { - opensearch_version = System.getProperty("opensearch.version", "1.0.0") + opensearch_version = System.getProperty("opensearch.version", "1.1.0") kotlin_version = System.getProperty("kotlin.version", "1.4.32") junit_version = System.getProperty("junit.version", "5.7.2") } diff --git a/notifications/gradle.properties b/notifications/gradle.properties index 9fe7a004..5d9783db 100644 --- a/notifications/gradle.properties +++ b/notifications/gradle.properties @@ -25,4 +25,4 @@ # # -version = 1.0.0 +version = 1.1.0 diff --git a/notifications/notifications/build.gradle b/notifications/notifications/build.gradle index 888f419a..638e5158 100644 --- a/notifications/notifications/build.gradle +++ b/notifications/notifications/build.gradle @@ -146,6 +146,11 @@ afterEvaluate { } test { + if (project.hasProperty('excludeTests')) { + project.properties['excludeTests']?.replaceAll('\\s', '')?.split('[,;]')?.each { + exclude "${it}" + } + } systemProperty 'tests.security.manager', 'false' useJUnitPlatform() } From 29d86faffc13d1ea52fa2f8c11f73f6a0370d5e2 Mon Sep 17 00:00:00 2001 From: Zhongnan Su Date: Mon, 9 Aug 2021 09:37:56 -0700 Subject: [PATCH 10/29] add support for email credentials (#252) --- .../send/SendMessageActionHelper.kt | 7 +- .../spi/NotificationSpiPlugin.kt | 9 ++- .../spi/client/DestinationSmtpClient.kt | 34 ++++++++- .../spi/model/SecureDestinationSettings.kt | 16 +++++ .../spi/model/destination/SmtpDestination.kt | 1 + .../spi/setting/PluginSettings.kt | 69 ++++++++++++++++++- .../notifications/spi/SmtpDestinationTests.kt | 68 +++++++++++------- .../spi/integTest/SmtpEmailIT.kt | 2 + 8 files changed, 177 insertions(+), 29 deletions(-) create mode 100644 notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/SecureDestinationSettings.kt 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 d7a93d11..ef08e45d 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 @@ -243,15 +243,16 @@ object SendMessageActionHelper { message: MessageContent, eventStatus: EventStatus ): EventStatus { - val smtpAccount = childConfigs.find { it.docInfo.id == email.emailAccountID } + val smtpAccountDocInfo = childConfigs.find { it.docInfo.id == email.emailAccountID } val groups = childConfigs.filter { email.emailGroupIds.contains(it.docInfo.id) } val groupRecipients = groups.map { (it.configDoc.config.configData as EmailGroup).recipients }.flatten() val recipients = email.recipients.union(groupRecipients) val emailRecipientStatus: List + val smtpAccountConfig = smtpAccountDocInfo?.configDoc!!.config runBlocking { val statusDeferredList = recipients.map { async(Dispatchers.IO) { - sendEmailFromSmtpAccount(smtpAccount?.configDoc?.config?.configData as SmtpAccount, it, message) + sendEmailFromSmtpAccount(smtpAccountConfig.name, smtpAccountConfig.configData as SmtpAccount, it, message) } } emailRecipientStatus = statusDeferredList.awaitAll() @@ -278,11 +279,13 @@ object SendMessageActionHelper { */ @Suppress("UnusedPrivateMember") private fun sendEmailFromSmtpAccount( + accountName: String, smtpAccount: SmtpAccount, recipient: String, message: MessageContent ): EmailRecipientStatus { val destination = SmtpDestination( + accountName, smtpAccount.host, smtpAccount.port, smtpAccount.method.tag, diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/NotificationSpiPlugin.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/NotificationSpiPlugin.kt index d97f17db..54f695d4 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/NotificationSpiPlugin.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/NotificationSpiPlugin.kt @@ -16,12 +16,15 @@ import org.opensearch.cluster.metadata.IndexNameExpressionResolver import org.opensearch.cluster.service.ClusterService import org.opensearch.common.io.stream.NamedWriteableRegistry import org.opensearch.common.settings.Setting +import org.opensearch.common.settings.Settings import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.env.Environment import org.opensearch.env.NodeEnvironment import org.opensearch.notifications.spi.setting.PluginSettings +import org.opensearch.notifications.spi.setting.PluginSettings.loadDestinationSettings import org.opensearch.notifications.spi.utils.logger import org.opensearch.plugins.Plugin +import org.opensearch.plugins.ReloadablePlugin import org.opensearch.repositories.RepositoriesService import org.opensearch.script.ScriptService import org.opensearch.threadpool.ThreadPool @@ -31,7 +34,7 @@ import java.util.function.Supplier /** * This is a dummy plugin for SPI to load configurations */ -internal class NotificationSpiPlugin : Plugin() { +internal class NotificationSpiPlugin : ReloadablePlugin, Plugin() { lateinit var clusterService: ClusterService // initialized in createComponents() internal companion object { @@ -69,4 +72,8 @@ internal class NotificationSpiPlugin : Plugin() { PluginSettings.addSettingsUpdateConsumer(clusterService) return listOf() } + + override fun reload(settings: Settings) { + PluginSettings.destinationSettings = loadDestinationSettings(settings) + } } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSmtpClient.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSmtpClient.kt index 08312913..ca41d839 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSmtpClient.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSmtpClient.kt @@ -12,16 +12,20 @@ package org.opensearch.notifications.spi.client import com.sun.mail.util.MailConnectException +import org.opensearch.common.settings.SecureString import org.opensearch.notifications.spi.model.DestinationMessageResponse import org.opensearch.notifications.spi.model.MessageContent +import org.opensearch.notifications.spi.model.SecureDestinationSettings import org.opensearch.notifications.spi.model.destination.SmtpDestination import org.opensearch.notifications.spi.setting.PluginSettings import org.opensearch.notifications.spi.utils.SecurityAccess import org.opensearch.notifications.spi.utils.logger import org.opensearch.rest.RestStatus import java.util.Properties +import javax.mail.Authenticator import javax.mail.Message import javax.mail.MessagingException +import javax.mail.PasswordAuthentication import javax.mail.SendFailedException import javax.mail.Session import javax.mail.Transport @@ -49,7 +53,7 @@ class DestinationSmtpClient { prop["mail.transport.protocol"] = "smtp" prop["mail.smtp.host"] = smtpDestination.host prop["mail.smtp.port"] = smtpDestination.port - val session = Session.getInstance(prop) + var session = Session.getInstance(prop) when (smtpDestination.method) { "ssl" -> prop["mail.smtp.ssl.enable"] = true @@ -58,6 +62,24 @@ class DestinationSmtpClient { else -> throw IllegalArgumentException("Invalid method supplied") } + if (smtpDestination.method != "none") { + val secureDestinationSetting = getSecureDestinationSetting(smtpDestination) + if (secureDestinationSetting != null) { + prop["mail.smtp.auth"] = true + session = Session.getInstance( + prop, + object : Authenticator() { + override fun getPasswordAuthentication(): PasswordAuthentication { + return PasswordAuthentication( + secureDestinationSetting.emailUsername.toString(), + secureDestinationSetting.emailPassword.toString() + ) + } + } + ) + } + } + // prepare mimeMessage val mimeMessage = EmailMimeProvider.prepareMimeMessage(session, smtpDestination, message) @@ -65,6 +87,16 @@ class DestinationSmtpClient { return sendMimeMessage(mimeMessage) } + fun getSecureDestinationSetting(SmtpDestination: SmtpDestination): SecureDestinationSettings? { + val emailUsername: SecureString? = PluginSettings.destinationSettings[SmtpDestination.accountName]?.emailUsername + val emailPassword: SecureString? = PluginSettings.destinationSettings[SmtpDestination.accountName]?.emailPassword + return if (emailUsername == null || emailPassword == null) { + null + } else { + SecureDestinationSettings(emailUsername, emailPassword) + } + } + /** * {@inheritDoc} */ diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/SecureDestinationSettings.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/SecureDestinationSettings.kt new file mode 100644 index 00000000..4cfb11cd --- /dev/null +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/SecureDestinationSettings.kt @@ -0,0 +1,16 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.spi.model + +import org.opensearch.common.settings.SecureString + +data class SecureDestinationSettings(val emailUsername: SecureString, val emailPassword: SecureString) diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SmtpDestination.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SmtpDestination.kt index 39526ce0..ac76bf71 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SmtpDestination.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SmtpDestination.kt @@ -34,6 +34,7 @@ import org.opensearch.notifications.spi.utils.validateEmail * This class holds the contents of smtp destination */ class SmtpDestination( + val accountName: String, val host: String, val port: Int, val method: String, diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/setting/PluginSettings.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/setting/PluginSettings.kt index ac8e9046..37e17bf5 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/setting/PluginSettings.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/setting/PluginSettings.kt @@ -13,12 +13,15 @@ package org.opensearch.notifications.spi.setting import org.opensearch.bootstrap.BootstrapInfo import org.opensearch.cluster.service.ClusterService +import org.opensearch.common.settings.SecureSetting +import org.opensearch.common.settings.SecureString import org.opensearch.common.settings.Setting import org.opensearch.common.settings.Setting.Property.Dynamic import org.opensearch.common.settings.Setting.Property.NodeScope import org.opensearch.common.settings.Settings import org.opensearch.notifications.spi.NotificationSpiPlugin.Companion.LOG_PREFIX import org.opensearch.notifications.spi.NotificationSpiPlugin.Companion.PLUGIN_NAME +import org.opensearch.notifications.spi.model.SecureDestinationSettings import org.opensearch.notifications.spi.utils.logger import java.io.IOException import java.nio.file.Path @@ -34,6 +37,11 @@ internal object PluginSettings { */ private const val EMAIL_KEY_PREFIX = "$KEY_PREFIX.email" + /** + * Settings Key prefix for Email. + */ + private const val EMAIL_DESTINATION_SETTING_PREFIX = "$KEY_PREFIX.email." + /** * Settings Key prefix for http connection. */ @@ -141,6 +149,11 @@ internal object PluginSettings { */ private const val DEFAULT_TOOLTIP_SUPPORT = false + /** + * Default destination settings + */ + private val DEFAULT_DESTINATION_SETTINGS = emptyMap() + /** * list of allowed config types. */ @@ -195,6 +208,12 @@ internal object PluginSettings { @Volatile var hostDenyList: List + /** + * Destination Settings + */ + @Volatile + var destinationSettings: Map + private const val DECIMAL_RADIX: Int = 10 private val log by logger(javaClass) @@ -224,6 +243,7 @@ internal object PluginSettings { allowedConfigTypes = settings?.getAsList(ALLOWED_CONFIG_TYPE_KEY, null) ?: DEFAULT_ALLOWED_CONFIG_TYPES tooltipSupport = settings?.getAsBoolean(TOOLTIP_SUPPORT_KEY, false) ?: DEFAULT_TOOLTIP_SUPPORT hostDenyList = settings?.getAsList(HOST_DENY_LIST_KEY, null) ?: DEFAULT_HOST_DENY_LIST + destinationSettings = if (settings != null) loadDestinationSettings(settings) else DEFAULT_DESTINATION_SETTINGS defaultSettings = mapOf( EMAIL_SIZE_LIMIT_KEY to emailSizeLimit.toString(DECIMAL_RADIX), @@ -293,6 +313,18 @@ internal object PluginSettings { NodeScope, Dynamic ) + private val EMAIL_USERNAME: Setting.AffixSetting = Setting.affixKeySetting( + EMAIL_DESTINATION_SETTING_PREFIX, + "username", + { key: String -> SecureSetting.secureString(key, null) } + ) + + private val EMAIL_PASSWORD: Setting.AffixSetting = Setting.affixKeySetting( + EMAIL_DESTINATION_SETTING_PREFIX, + "password", + { key: String -> SecureSetting.secureString(key, null) } + ) + /** * Returns list of additional settings available specific to this plugin. * @@ -308,7 +340,9 @@ internal object PluginSettings { SOCKET_TIMEOUT_MILLISECONDS, ALLOWED_CONFIG_TYPES, TOOLTIP_SUPPORT, - HOST_DENY_LIST + HOST_DENY_LIST, + EMAIL_USERNAME, + EMAIL_PASSWORD ) } /** @@ -427,4 +461,37 @@ internal object PluginSettings { log.info("$LOG_PREFIX:$HOST_DENY_LIST_KEY -updatedTo-> $it") } } + + fun loadDestinationSettings(settings: Settings): Map { + // Only loading Email Destination settings for now since those are the only secure settings needed. + // If this logic needs to be expanded to support other Destinations, different groups can be retrieved similar + // to emailAccountNames based on the setting namespace and SecureDestinationSettings should be expanded to support + // these new settings. + val emailAccountNames: Set = settings.getGroups(EMAIL_DESTINATION_SETTING_PREFIX).keys + val emailAccounts: MutableMap = mutableMapOf() + for (emailAccountName in emailAccountNames) { + // Only adding the settings if they exist + getSecureDestinationSettings(settings, emailAccountName)?.let { + emailAccounts[emailAccountName] = it + } + } + + return emailAccounts + } + + private fun getSecureDestinationSettings(settings: Settings, emailAccountName: String): SecureDestinationSettings? { + // Using 'use' to emulate Java's try-with-resources on multiple closeable resources. + // Values are cloned so that we maintain a SecureString, the original SecureStrings will be closed after + // they have left the scope of this function. + return getEmailSettingValue(settings, emailAccountName, EMAIL_USERNAME)?.use { emailUsername -> + getEmailSettingValue(settings, emailAccountName, EMAIL_PASSWORD)?.use { emailPassword -> + SecureDestinationSettings(emailUsername = emailUsername.clone(), emailPassword = emailPassword.clone()) + } + } + } + + private fun getEmailSettingValue(settings: Settings, emailAccountName: String, emailSetting: Setting.AffixSetting): T? { + val concreteSetting = emailSetting.getConcreteSettingForNamespace(emailAccountName) + return concreteSetting.get(settings) + } } diff --git a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/SmtpDestinationTests.kt b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/SmtpDestinationTests.kt index 8d0ac9ae..5a431e58 100644 --- a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/SmtpDestinationTests.kt +++ b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/SmtpDestinationTests.kt @@ -13,14 +13,16 @@ package org.opensearch.notifications.spi import io.mockk.every import io.mockk.spyk -import junit.framework.Assert.assertEquals -import org.junit.Assert -import org.junit.Test +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.mockito.junit.jupiter.MockitoExtension +import org.opensearch.common.settings.SecureString import org.opensearch.notifications.spi.client.DestinationSmtpClient import org.opensearch.notifications.spi.model.DestinationMessageResponse import org.opensearch.notifications.spi.model.MessageContent +import org.opensearch.notifications.spi.model.SecureDestinationSettings import org.opensearch.notifications.spi.model.destination.DestinationType import org.opensearch.notifications.spi.model.destination.SmtpDestination import org.opensearch.notifications.spi.transport.DestinationTransportProvider @@ -32,7 +34,6 @@ import javax.mail.MessagingException internal class SmtpDestinationTests { @Test - @Throws(Exception::class) fun testSmtpEmailMessage() { val expectedEmailResponse = DestinationMessageResponse(RestStatus.OK.status, "Success") val emailClient = spyk() @@ -46,7 +47,32 @@ internal class SmtpDestinationTests { "link test: http://sample.com email test: marymajor@example.com All member callout: " + "@All All Present member callout: @Present}" val message = MessageContent(subject, messageText) - val destination = SmtpDestination("abc", 465, "ssl", "test@abc.com", "to@abc.com") + val destination = SmtpDestination("testAccountName", "abc", 465, "ssl", "test@abc.com", "to@abc.com") + + val actualEmailResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message) + assertEquals(expectedEmailResponse.statusCode, actualEmailResponse.statusCode) + assertEquals(expectedEmailResponse.statusText, actualEmailResponse.statusText) + } + + @Test + fun `test auth email`() { + val expectedEmailResponse = DestinationMessageResponse(RestStatus.OK.status, "Success") + val emailClient = spyk() + every { emailClient.sendMessage(any()) } returns Unit + + val username = SecureString("user1".toCharArray()) + val password = SecureString("password".toCharArray()) + every { emailClient.getSecureDestinationSetting(any()) } returns SecureDestinationSettings(username, password) + + val smtpDestinationTransport = SmtpDestinationTransport(emailClient) + DestinationTransportProvider.destinationTransportMap = mapOf(DestinationType.SMTP to smtpDestinationTransport) + + val subject = "Test SMTP Email subject" + val messageText = "{Message gughjhjlkh Body emoji test: :) :+1: " + + "link test: http://sample.com email test: marymajor@example.com All member callout: " + + "@All All Present member callout: @Present}" + val message = MessageContent(subject, messageText) + val destination = SmtpDestination("testAccountName", "abc", 465, "ssl", "test@abc.com", "to@abc.com") val actualEmailResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message) assertEquals(expectedEmailResponse.statusCode, actualEmailResponse.statusCode) @@ -54,7 +80,6 @@ internal class SmtpDestinationTests { } @Test - @Throws(Exception::class) fun testSmtpFailingEmailMessage() { val expectedEmailResponse = DestinationMessageResponse( RestStatus.FAILED_DEPENDENCY.status, @@ -74,6 +99,7 @@ internal class SmtpDestinationTests { "@All All Present member callout: @Present}" val message = MessageContent(subject, messageText) val destination = SmtpDestination( + "testAccountName", "localhost", 55555, "none", @@ -87,33 +113,27 @@ internal class SmtpDestinationTests { assertEquals("sendEmail Error, status:${expectedEmailResponse.statusText}", actualEmailResponse.statusText) } - @Test(expected = IllegalArgumentException::class) + @Test fun testHostMissingEmailDestination() { - try { - SmtpDestination("", 465, "ssl", "from@test.com", "to@test.com") - } catch (exception: Exception) { - Assert.assertEquals("Host name should be provided", exception.message) - throw exception + val exception = Assertions.assertThrows(IllegalArgumentException::class.java) { + SmtpDestination("testAccountName", "", 465, "ssl", "from@test.com", "to@test.com") } + assertEquals("Host name should be provided", exception.message) } - @Test(expected = IllegalArgumentException::class) + @Test fun testInvalidPortEmailDestination() { - try { - SmtpDestination("localhost", -1, "ssl", "from@test.com", "to@test.com") - } catch (exception: Exception) { - Assert.assertEquals("Port should be positive value", exception.message) - throw exception + val exception = Assertions.assertThrows(IllegalArgumentException::class.java) { + SmtpDestination("testAccountName", "localhost", -1, "ssl", "from@test.com", "to@test.com") } + assertEquals("Port should be positive value", exception.message) } - @Test(expected = IllegalArgumentException::class) + @Test fun testMissingFromOrRecipientEmailDestination() { - try { - SmtpDestination("localhost", 465, "ssl", "", "to@test.com") - } catch (exception: Exception) { - Assert.assertEquals("FromAddress and recipient should be provided", exception.message) - throw exception + val exception = Assertions.assertThrows(IllegalArgumentException::class.java) { + SmtpDestination("testAccountName", "localhost", 465, "ssl", "", "to@test.com") } + assertEquals("FromAddress and recipient should be provided", exception.message) } } diff --git a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/integTest/SmtpEmailIT.kt b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/integTest/SmtpEmailIT.kt index a4b9064a..ea6cf48e 100644 --- a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/integTest/SmtpEmailIT.kt +++ b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/integTest/SmtpEmailIT.kt @@ -36,6 +36,7 @@ internal class SmtpEmailIT : OpenSearchRestTestCase() { fun `test send email to one recipient over smtp server`() { val smtpDestination = SmtpDestination( + "testAccountName", "localhost", smtpPort, "none", @@ -58,6 +59,7 @@ internal class SmtpEmailIT : OpenSearchRestTestCase() { fun `test send email with non-available host`() { val smtpDestination = SmtpDestination( + "testAccountName", "invalidHost", smtpPort, "none", From b80bd1e8a0775f21e2f19f460d35acde7332ad9b Mon Sep 17 00:00:00 2001 From: Kavitha Conjeevaram Mohan Date: Tue, 10 Aug 2021 10:41:35 -0700 Subject: [PATCH 11/29] Update NotificationEventIndexTest.kt --- .../NotificationEventIndexTest.kt | 72 ++++++++++++++----- 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/NotificationEventIndexTest.kt b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/NotificationEventIndexTest.kt index 5f315be7..c56a7f4d 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/NotificationEventIndexTest.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/NotificationEventIndexTest.kt @@ -1,34 +1,41 @@ package org.opensearch.notifications.index -import org.junit.jupiter.api.Test -import org.mockito.Mock -import org.opensearch.action.get.GetRequest -import org.opensearch.client.Client -import org.opensearch.cluster.service.ClusterService -import org.opensearch.notifications.settings.PluginSettings -import org.junit.jupiter.api.BeforeEach -import com.nhaarman.mockitokotlin2.whenever import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.whenever import junit.framework.Assert.assertEquals +import org.apache.logging.log4j.Logger +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test import org.mockito.Mockito import org.mockito.Mockito.* -import org.mockito.MockitoAnnotations -import org.mockito.stubbing.OngoingStubbing import org.opensearch.action.ActionFuture import org.opensearch.action.admin.indices.create.CreateIndexResponse +import org.opensearch.action.get.GetRequest import org.opensearch.action.get.GetResponse import org.opensearch.action.support.master.AcknowledgedResponse import org.opensearch.client.AdminClient +import org.opensearch.client.Client import org.opensearch.client.IndicesAdminClient import org.opensearch.cluster.ClusterState import org.opensearch.cluster.routing.RoutingTable +import org.opensearch.cluster.service.ClusterService +import org.opensearch.common.collect.MapBuilder +import org.opensearch.common.settings.Setting +import org.opensearch.common.settings.Settings +import org.opensearch.common.unit.ByteSizeValue +import org.opensearch.common.util.concurrent.ThreadContext +import org.opensearch.common.util.concurrent.ThreadContext.StoredContext +//import org.opensearch.common.util.concurrent.ThreadContext.ThreadContextStruct import org.opensearch.commons.notifications.model.* +import org.opensearch.http.HttpTransportSettings import org.opensearch.notifications.model.DocInfo import org.opensearch.notifications.model.DocMetadata import org.opensearch.notifications.model.NotificationEventDoc import org.opensearch.notifications.model.NotificationEventDocInfo +import org.opensearch.notifications.util.SecureIndexClient +import org.opensearch.threadpool.ThreadPool import java.time.Instant +import java.util.stream.Collector internal class NotificationEventIndexTest{ @@ -46,9 +53,12 @@ internal class NotificationEventIndexTest{ fun setUp() { client = mock(Client::class.java,"client") clusterService = mock(ClusterService::class.java, "clusterservice") + //val secureIndexClient = mock(SecureIndexClient::class.java) + //whenever(SecureIndexClient(client)).thenReturn(secureIndexClient) NotificationEventIndex.initialize(client, clusterService) } + @Test fun `index operation to get single event` () { val id = "index-1" @@ -79,11 +89,11 @@ internal class NotificationEventIndexTest{ val eventDoc = NotificationEventDoc(metadata, sampleEvent) val expectedEventDocInfo = NotificationEventDocInfo(docInfo, eventDoc) - val getRequest = GetRequest(INDEX_NAME).id(id) - val mockActionFuture:ActionFuture = mock(ActionFuture::class.java) as ActionFuture + // val getRequest = GetRequest(INDEX_NAME).id(id) + //val mockActionFuture:ActionFuture = mock(ActionFuture::class.java) as ActionFuture //whenever(NotificationEventIndex.client.get(any())).thenReturn(mockActionFuture) - whenever(client.get(getRequest)).thenReturn(mockActionFuture) + //whenever(client.get(getRequest)).thenReturn(mockActionFuture) val clusterState = mock(ClusterState::class.java) whenever(clusterService.state()).thenReturn(clusterState) @@ -97,6 +107,8 @@ internal class NotificationEventIndexTest{ //val actionFuture = NotificationEventIndex.client.admin().indices().create(request) + + val admin = mock(AdminClient::class.java) val indices = mock(IndicesAdminClient::class.java) val mockCreateClient:ActionFuture = mock(ActionFuture::class.java) as ActionFuture @@ -106,17 +118,39 @@ internal class NotificationEventIndexTest{ whenever(indices.create(any())).thenReturn(mockCreateClient) //val time = PluginSettings.operationTimeoutMs - val mockActionGet = mockCreateClient.actionGet(PluginSettings.operationTimeoutMs) + val mockActionGet = mock(CreateIndexResponse::class.java) + + // mockCreateClient.actionGet(PluginSettings.operationTimeoutMs) whenever(mockCreateClient.actionGet(anyLong())).thenReturn(mockActionGet) println("mockActionGet: $mockActionGet") - println("mockCreateClient: $mockCreateClient") + + //println("mockCreateClient: $mockCreateClient") //println("plugin timout: $time") - //val mockResponse = mock(AcknowledgedResponse::class.java) - //whenever(response.isAcknowledged).thenReturn(mockResponse) + val mockResponse = mock(AcknowledgedResponse::class.java) + + //whenever(mockActionGet.isAcknowledged).thenReturn(mockResponse.isAcknowledged) + //whenever(mockActionGet.isAcknowledged).thenReturn(mockResponse) + //when(mockActionGet.isAcknowledged).thenReturn(true) + //doReturn(true).when(mockActionGet).isAcknowledged() + //Mockito.`when`(mockActionGet.isAcknowledged()).thenReturn(true) + + val getRequest = GetRequest(INDEX_NAME).id(id) + val mockActionFuture:ActionFuture = mock(ActionFuture::class.java) as ActionFuture + //whenever(client.get(any())).thenReturn(mockActionFuture) + + //client = mock(SecureIndexClient::class.java) + println("Mock action Future: $mockActionFuture") + whenever(client.get(getRequest)).thenReturn(mockActionFuture) + val mockThreadPool = mock(ThreadPool::class.java) + val mockThreadContext = mock(ThreadContext::class.java) + + whenever(client.threadPool()).thenReturn(mockThreadPool) + whenever(mockThreadPool.threadContext).thenReturn(mockThreadContext) + whenever(client.get(getRequest)).thenReturn(mockActionFuture) val actualEventDocInfo = NotificationEventIndex.getNotificationEvent(id) - verify(clusterService.state(), atLeast(1)) + //verify(clusterService.state(), atLeast(1)) verify(mockCreateClient.actionGet(), atLeast(1)) //verifyNoMoreInteractions() From a6fc0b5341e398ca508ad8989c34500462d1fe82 Mon Sep 17 00:00:00 2001 From: Zhongnan Su Date: Tue, 10 Aug 2021 16:43:35 -0700 Subject: [PATCH 12/29] downgrade to 1.0 and align common-utils in CI to use `notifiation-dev` branch (#271) --- .../workflows/notifications-test-and-build-workflow.yml | 8 ++++---- notifications/build.gradle | 2 +- notifications/gradle.properties | 2 +- .../notifications/send/SendMessageActionHelper.kt | 1 + 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/notifications-test-and-build-workflow.yml b/.github/workflows/notifications-test-and-build-workflow.yml index f1c3173f..daf6d600 100644 --- a/.github/workflows/notifications-test-and-build-workflow.yml +++ b/.github/workflows/notifications-test-and-build-workflow.yml @@ -26,9 +26,9 @@ name: Test and Build Notifications on: [push, pull_request] env: - OPENSEARCH_VERSION: '1.1' - COMMON_UTILS_BRANCH: 'main' - OPENSEARCH_BRANCH: 'main' + OPENSEARCH_VERSION: '1.0' + COMMON_UTILS_BRANCH: 'notification-dev' + OPENSEARCH_BRANCH: '1.0' jobs: build: @@ -56,7 +56,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'opensearch-project/common-utils' - ref: ${{ env.COMMON_UTILS_VERSION }} + ref: ${{ env.COMMON_UTILS_BRANCH }} path: common-utils - name: Build common-utils working-directory: ./common-utils diff --git a/notifications/build.gradle b/notifications/build.gradle index 227030e9..f0979a7f 100644 --- a/notifications/build.gradle +++ b/notifications/build.gradle @@ -28,7 +28,7 @@ buildscript { ext { - opensearch_version = System.getProperty("opensearch.version", "1.1.0") + opensearch_version = System.getProperty("opensearch.version", "1.0.0") kotlin_version = System.getProperty("kotlin.version", "1.4.32") junit_version = System.getProperty("junit.version", "5.7.2") } diff --git a/notifications/gradle.properties b/notifications/gradle.properties index 5d9783db..9fe7a004 100644 --- a/notifications/gradle.properties +++ b/notifications/gradle.properties @@ -25,4 +25,4 @@ # # -version = 1.1.0 +version = 1.0.0 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 ef08e45d..2c59a727 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 @@ -179,6 +179,7 @@ object SendMessageActionHelper { ConfigType.EMAIL -> sendEmailMessage(configData as Email, childConfigs, message, eventStatus) ConfigType.SMTP_ACCOUNT -> null ConfigType.EMAIL_GROUP -> null + ConfigType.SNS -> null } return if (response == null) { log.warn("Cannot send message to destination for config id :${channel.docInfo.id}") From e67201f83b575d23a93c32e456a270304b3e9d16 Mon Sep 17 00:00:00 2001 From: Joshua Date: Wed, 11 Aug 2021 11:27:14 -0700 Subject: [PATCH 13/29] Add SNS support in notifications plugin (#266) --- notifications/build.gradle | 1 + .../send/SendMessageActionHelper.kt | 12 +++++ .../plugin-metadata/plugin-security.policy | 25 +++++++++ .../notifications-config-mapping.yml | 7 +++ notifications/spi/build.gradle | 13 ++++- .../spi/client/DestinationClientPool.kt | 7 +++ .../spi/client/DestinationSNSClient.kt | 30 +++++++++++ .../spi/credentials/CredentialsProvider.kt | 19 +++++++ .../spi/credentials/SNSClient.kt | 19 +++++++ .../oss/CredentialsProviderFactory.kt | 51 +++++++++++++++++++ .../spi/credentials/oss/SNSClientFactory.kt | 27 ++++++++++ .../spi/model/destination/SNSDestination.kt | 28 ++++++++++ .../spi/setting/PluginSettings.kt | 1 + .../transport/DestinationTransportProvider.kt | 6 ++- .../spi/transport/SNSDestinationTransport.kt | 42 +++++++++++++++ 15 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSNSClient.kt create mode 100644 notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/CredentialsProvider.kt create mode 100644 notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SNSClient.kt create mode 100644 notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/CredentialsProviderFactory.kt create mode 100644 notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SNSClientFactory.kt create mode 100644 notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SNSDestination.kt create mode 100644 notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SNSDestinationTransport.kt diff --git a/notifications/build.gradle b/notifications/build.gradle index f0979a7f..67089a27 100644 --- a/notifications/build.gradle +++ b/notifications/build.gradle @@ -31,6 +31,7 @@ buildscript { opensearch_version = System.getProperty("opensearch.version", "1.0.0") kotlin_version = System.getProperty("kotlin.version", "1.4.32") junit_version = System.getProperty("junit.version", "5.7.2") + aws_version = System.getProperty("aws.version", "1.12.20") } repositories { 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 2c59a727..be22ce9d 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 @@ -28,6 +28,7 @@ import org.opensearch.commons.notifications.model.EmailRecipientStatus import org.opensearch.commons.notifications.model.EventSource import org.opensearch.commons.notifications.model.EventStatus import org.opensearch.commons.notifications.model.NotificationEvent +import org.opensearch.commons.notifications.model.SNS import org.opensearch.commons.notifications.model.Slack import org.opensearch.commons.notifications.model.SmtpAccount import org.opensearch.commons.notifications.model.Webhook @@ -45,6 +46,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.SNSDestination import org.opensearch.notifications.spi.model.destination.SlackDestination import org.opensearch.notifications.spi.model.destination.SmtpDestination import org.opensearch.rest.RestStatus @@ -177,6 +179,7 @@ object SendMessageActionHelper { ConfigType.CHIME -> sendChimeMessage(configData as Chime, message, eventStatus) ConfigType.WEBHOOK -> sendWebhookMessage(configData as Webhook, message, eventStatus) ConfigType.EMAIL -> sendEmailMessage(configData as Email, childConfigs, message, eventStatus) + ConfigType.SNS -> sendSNSMessage(configData as SNS, message, eventStatus) ConfigType.SMTP_ACCOUNT -> null ConfigType.EMAIL_GROUP -> null ConfigType.SNS -> null @@ -300,6 +303,15 @@ object SendMessageActionHelper { ) } + /** + * send message to SNS destination + */ + private fun sendSNSMessage(sns: SNS, message: MessageContent, eventStatus: EventStatus): EventStatus { + val destination = SNSDestination(sns.topicARN, sns.roleARN) + val status = sendMessageThroughSpi(destination, message) + return eventStatus.copy(deliveryStatus = DeliveryStatus(status.statusCode.toString(), status.statusText)) + } + /** * Send message to destination using SPI */ diff --git a/notifications/notifications/src/main/plugin-metadata/plugin-security.policy b/notifications/notifications/src/main/plugin-metadata/plugin-security.policy index ba91b1a4..6a673e96 100644 --- a/notifications/notifications/src/main/plugin-metadata/plugin-security.policy +++ b/notifications/notifications/src/main/plugin-metadata/plugin-security.policy @@ -33,4 +33,29 @@ grant { permission java.net.SocketPermission "*", "connect,resolve"; permission java.net.NetPermission "getProxySelector"; permission java.io.FilePermission "${user.home}${/}.aws${/}*", "read"; + + // https://github.com/lezzago/alerting/blob/374a379f525d4638969890d15b913179e7afd122/alerting/src/main/plugin-metadata/plugin-security.policy + // 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/notifications/notifications/src/main/resources/notifications-config-mapping.yml b/notifications/notifications/src/main/resources/notifications-config-mapping.yml index 026ed3f8..8f1086b5 100644 --- a/notifications/notifications/src/main/resources/notifications-config-mapping.yml +++ b/notifications/notifications/src/main/resources/notifications-config-mapping.yml @@ -100,6 +100,13 @@ properties: type: keyword email_group_id_list: type: keyword + sns: # sns configuration + type: object + properties: + topic_arn: + type: keyword + role_arn: + type: keyword smtp_account: # smtp account configuration type: object properties: diff --git a/notifications/spi/build.gradle b/notifications/spi/build.gradle index 44f92537..48c0a652 100644 --- a/notifications/spi/build.gradle +++ b/notifications/spi/build.gradle @@ -95,6 +95,14 @@ configurations.all { force "io.netty:netty-handler:4.1.63.Final" // resolve for awssdk:ses force "org.apache.httpcomponents:httpclient:4.5.10" // resolve for awssdk:ses force "org.apache.httpcomponents:httpcore:4.4.13" // resolve for awssdk:ses + + // Resolve for awssdk:sns + force "joda-time:joda-time:2.8.1" + force "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.12.3" + force "com.fasterxml.jackson.core:jackson-core:2.12.3" + force "com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.12.3" + force "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.12.3" + force "junit:junit:4.12" } } @@ -104,6 +112,8 @@ dependencies { compileOnly "org.jetbrains.kotlin:kotlin-stdlib-common:${kotlin_version}" compile "org.apache.httpcomponents:httpcore:4.4.5" compile "org.apache.httpcomponents:httpclient:4.5.10" + compile "com.amazonaws:aws-java-sdk-sns:${aws_version}" + compile "com.amazonaws:aws-java-sdk-sts:${aws_version}" //TODO: Add it back and remove from main project(to avoid jarhell) when implementing Email functionality // compile ("software.amazon.awssdk:ses:2.14.16") { // exclude module: 'annotations' // conflict with org.jetbrains:annotations, integTestRunner fails with error "codebase property already set" @@ -143,7 +153,8 @@ configurations { shadowJar { // fix jarhell by relocating packages - relocate 'com.fasterxml.jackson.core', 'org.opensearch.notifications.repackage.com.fasterxml.jackson.core' + relocate 'org.joda.time', 'org.opensearch.notifications.repackage.org.joda.time' + relocate 'com.fasterxml.jackson', 'org.opensearch.notifications.repackage.com.fasterxml.jackson' relocate 'org.apache.http', 'org.opensearch.notifications.repackage.org.apache.http' relocate 'org.apache.commons.logging', 'org.opensearch.notifications.repackage.org.apache.commons.logging' relocate 'org.apache.commons.codec', 'org.opensearch.notifications.repackage.org.apache.commons.codec' diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationClientPool.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationClientPool.kt index 84ebc44d..ecd622f8 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationClientPool.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationClientPool.kt @@ -27,10 +27,17 @@ package org.opensearch.notifications.spi.client +import org.opensearch.notifications.spi.model.destination.SNSDestination + /** * This class provides Client to the relevant destinations */ internal object DestinationClientPool { val httpClient: DestinationHttpClient = DestinationHttpClient() val smtpClient: DestinationSmtpClient = DestinationSmtpClient() + + // TODO: cache by cred and region? + fun getSNSClient(destination: SNSDestination): DestinationSNSClient { + return DestinationSNSClient(destination) + } } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSNSClient.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSNSClient.kt new file mode 100644 index 00000000..e9a88784 --- /dev/null +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSNSClient.kt @@ -0,0 +1,30 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.spi.client + +import com.amazonaws.services.sns.AmazonSNS +import org.opensearch.notifications.spi.credentials.oss.SNSClientFactory +import org.opensearch.notifications.spi.model.MessageContent +import org.opensearch.notifications.spi.model.destination.SNSDestination + +/** + * This class handles the SNS connections to the given Destination. + */ +class DestinationSNSClient(destination: SNSDestination) { + + private val amazonSNS: AmazonSNS = SNSClientFactory().getClient(destination) + + fun execute(topicArn: String, message: MessageContent): String { + val result = amazonSNS.publish(topicArn, message.textDescription, message.title) + return result.messageId + } +} diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/CredentialsProvider.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/CredentialsProvider.kt new file mode 100644 index 00000000..5b30dfb9 --- /dev/null +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/CredentialsProvider.kt @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.spi.credentials + +import com.amazonaws.auth.AWSCredentialsProvider +import org.opensearch.notifications.spi.model.destination.SNSDestination + +interface CredentialsProvider { + fun getCredentialsProvider(destination: SNSDestination): AWSCredentialsProvider +} diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SNSClient.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SNSClient.kt new file mode 100644 index 00000000..80c1e0e8 --- /dev/null +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SNSClient.kt @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.spi.credentials + +import com.amazonaws.services.sns.AmazonSNS +import org.opensearch.notifications.spi.model.destination.SNSDestination + +interface SNSClient { + fun getClient(destination: SNSDestination): AmazonSNS +} diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/CredentialsProviderFactory.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/CredentialsProviderFactory.kt new file mode 100644 index 00000000..82a4effe --- /dev/null +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/CredentialsProviderFactory.kt @@ -0,0 +1,51 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.spi.credentials.oss + +import com.amazonaws.auth.AWSCredentialsProvider +import com.amazonaws.auth.AWSStaticCredentialsProvider +import com.amazonaws.auth.BasicSessionCredentials +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain +import com.amazonaws.auth.profile.ProfileCredentialsProvider +import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder +import com.amazonaws.services.securitytoken.model.AssumeRoleRequest +import org.opensearch.notifications.spi.credentials.CredentialsProvider +import org.opensearch.notifications.spi.model.destination.SNSDestination + +class CredentialsProviderFactory : CredentialsProvider { + override fun getCredentialsProvider(destination: SNSDestination): AWSCredentialsProvider { + return if (destination.roleArn != null) { + getCredentialsProviderByIAMRole(destination) + } else { + DefaultAWSCredentialsProviderChain() + } + } + + private fun getCredentialsProviderByIAMRole(destination: SNSDestination): AWSCredentialsProvider { + // TODO cache credentials by role ARN? + val stsClient = AWSSecurityTokenServiceClientBuilder.standard() + .withCredentials(ProfileCredentialsProvider()) + .withRegion(destination.getRegion()) + .build() + val roleRequest = AssumeRoleRequest() + .withRoleArn(destination.roleArn) + .withRoleSessionName("opensearch-notifications") + val roleResponse = stsClient.assumeRole(roleRequest) + val sessionCredentials = roleResponse.credentials + val awsCredentials = BasicSessionCredentials( + sessionCredentials.accessKeyId, + sessionCredentials.secretAccessKey, + sessionCredentials.sessionToken + ) + return AWSStaticCredentialsProvider(awsCredentials) + } +} diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SNSClientFactory.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SNSClientFactory.kt new file mode 100644 index 00000000..27e296d9 --- /dev/null +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SNSClientFactory.kt @@ -0,0 +1,27 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.spi.credentials.oss + +import com.amazonaws.services.sns.AmazonSNS +import com.amazonaws.services.sns.AmazonSNSClientBuilder +import org.opensearch.notifications.spi.credentials.SNSClient +import org.opensearch.notifications.spi.model.destination.SNSDestination + +class SNSClientFactory : SNSClient { + override fun getClient(destination: SNSDestination): AmazonSNS { + val credentials = CredentialsProviderFactory().getCredentialsProvider(destination) + return AmazonSNSClientBuilder.standard() + .withRegion(destination.getRegion()) + .withCredentials(credentials) + .build() + } +} diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SNSDestination.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SNSDestination.kt new file mode 100644 index 00000000..19ef096b --- /dev/null +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SNSDestination.kt @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.notifications.spi.model.destination + +/** + * This class holds the contents of SNS destination + */ +data class SNSDestination( + val topicArn: String, + val roleArn: String? = null, +) : BaseDestination(DestinationType.SNS) { + + /** + * Get AWS region from topic arn + */ + fun getRegion(): String { + // sample topic arn arn:aws:sns:us-west-2:075315751589:test-notification + return topicArn.split(":".toRegex()).toTypedArray()[3] + } +} diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/setting/PluginSettings.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/setting/PluginSettings.kt index 37e17bf5..3a0c8cdf 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/setting/PluginSettings.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/setting/PluginSettings.kt @@ -135,6 +135,7 @@ internal object PluginSettings { "chime", "webhook", "email", + "sns", "smtp_account", "email_group" ) diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransportProvider.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransportProvider.kt index 8e58824b..6d19efa0 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransportProvider.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransportProvider.kt @@ -23,14 +23,16 @@ internal object DestinationTransportProvider { private val webhookDestinationTransport = WebhookDestinationTransport() private val smtpDestinationTransport = SmtpDestinationTransport() + private val snsDestinationTransport = SNSDestinationTransport() @OpenForTesting var destinationTransportMap = mapOf( - // TODO Add other destinations, ses, sns + // TODO Add other destinations, ses DestinationType.SLACK to webhookDestinationTransport, DestinationType.CHIME to webhookDestinationTransport, DestinationType.CUSTOM_WEBHOOK to webhookDestinationTransport, - DestinationType.SMTP to smtpDestinationTransport + DestinationType.SMTP to smtpDestinationTransport, + DestinationType.SNS to snsDestinationTransport ) /** diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SNSDestinationTransport.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SNSDestinationTransport.kt new file mode 100644 index 00000000..a63c390a --- /dev/null +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SNSDestinationTransport.kt @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.spi.transport + +import org.opensearch.notifications.spi.client.DestinationClientPool +import org.opensearch.notifications.spi.model.DestinationMessageResponse +import org.opensearch.notifications.spi.model.MessageContent +import org.opensearch.notifications.spi.model.destination.SNSDestination +import org.opensearch.notifications.spi.utils.logger +import org.opensearch.rest.RestStatus +import java.io.IOException + +/** + * This class handles the client responsible for submitting the messages to SNS destinations. + */ +internal class SNSDestinationTransport : DestinationTransport { + + private val log by logger(SNSDestinationTransport::class.java) + + override fun sendMessage(destination: SNSDestination, message: MessageContent): DestinationMessageResponse { + return try { + val snsClient = DestinationClientPool.getSNSClient(destination) + val response = snsClient.execute(destination.topicArn, message) + DestinationMessageResponse(RestStatus.OK.status, "Success, message id: $response") + } catch (exception: IOException) { + log.error("Exception sending message: $message", exception) + DestinationMessageResponse( + RestStatus.INTERNAL_SERVER_ERROR.status, + "Failed to send message ${exception.message}" + ) + } + } +} From 9a0458f590f1cb8fa98cbdefebf3ed3303669de5 Mon Sep 17 00:00:00 2001 From: Anantha Krishna Bhatta Date: Tue, 10 Aug 2021 16:35:13 -0700 Subject: [PATCH 14/29] Added SES channel in SPI resolved conflicts and refactored sns [Tests] Updated tests for the modified interface Signed-off-by: @akbhatta --- notifications/notifications/build.gradle | 14 - .../src/main/config/notifications.yml | 11 - .../notifications/NotificationPlugin.kt | 6 - .../notifications/action/SendMessageAction.kt | 128 -------- .../notifications/channel/ChannelFactory.kt | 56 ---- .../notifications/channel/ChannelProvider.kt | 40 --- .../notifications/channel/EmptyChannel.kt | 62 ---- .../channel/NotificationChannel.kt | 67 ---- .../channel/email/BaseEmailChannel.kt | 167 ---------- .../channel/email/EmailChannelFactory.kt | 52 --- .../channel/email/EmailMimeProvider.kt | 159 ---------- .../notifications/channel/email/SesChannel.kt | 138 -------- .../channel/email/SmtpChannel.kt | 93 ------ .../index/ConfigIndexingActions.kt | 7 + .../notifications/index/ConfigQueryHelper.kt | 7 +- .../NotificationConfigRestHandler.kt | 2 + .../resthandler/SendMessageRestHandler.kt | 91 ------ .../send/SendMessageActionHelper.kt | 86 +++-- .../notifications/settings/PluginSettings.kt | 298 +----------------- .../notifications/throttle/Accountant.kt | 68 ---- .../notifications/throttle/CounterIndex.kt | 253 --------------- .../throttle/CounterIndexModel.kt | 190 ----------- .../notifications/throttle/Counters.kt | 67 ---- .../throttle/EmptyMessageCounter.kt | 49 --- .../notifications/throttle/MessageCounter.kt | 49 --- .../notifications-config-mapping.yml | 10 +- .../integtest/NotificationsRestTestCase.kt | 126 -------- .../integtest/channel/SesChannelIT.kt | 72 ----- .../integtest/channel/SmtpChannelIT.kt | 78 ----- .../SendMessageRestHandlerTests.kt | 54 ---- notifications/spi/build.gradle | 25 +- .../notifications/spi/NotificationSpi.kt | 10 +- .../spi/client/DestinationClientPool.kt | 10 +- .../spi/client/DestinationHttpClient.kt | 12 +- .../spi/client/DestinationSesClient.kt | 153 +++++++++ .../spi/client/DestinationSmtpClient.kt | 48 ++- ...onSNSClient.kt => DestinationSnsClient.kt} | 13 +- .../spi/client/EmailMessageValidator.kt | 38 +++ .../spi/client/EmailMimeProvider.kt | 17 +- .../spi/credentials/CredentialsProvider.kt | 12 +- .../spi/credentials/SesClientFactory.kt | 21 ++ .../{SNSClient.kt => SnsClientFactory.kt} | 8 +- .../oss/CredentialsProviderFactory.kt | 13 +- .../spi/credentials/oss/SNSClientFactory.kt | 27 -- .../credentials/oss/SesClientFactoryImpl.kt | 32 ++ .../credentials/oss/SnsClientFactoryImpl.kt | 33 ++ .../spi/model/destination/SesDestination.kt | 34 ++ .../{SNSDestination.kt => SnsDestination.kt} | 12 +- .../spi/transport/DestinationTransport.kt | 5 +- .../transport/DestinationTransportProvider.kt | 7 +- .../spi/transport/SesDestinationTransport.kt | 78 +++++ .../spi/transport/SmtpDestinationTransport.kt | 10 +- ...ransport.kt => SnsDestinationTransport.kt} | 31 +- .../transport/WebhookDestinationTransport.kt | 10 +- .../spi/ChimeDestinationTests.kt | 18 +- .../spi/CustomWebhookDestinationTests.kt | 6 +- .../spi/SlackDestinationTests.kt | 6 +- .../notifications/spi/SmtpDestinationTests.kt | 6 +- .../spi/integTest/SmtpEmailIT.kt | 4 +- 59 files changed, 635 insertions(+), 2564 deletions(-) delete mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/SendMessageAction.kt delete mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/ChannelFactory.kt delete mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/ChannelProvider.kt delete mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/EmptyChannel.kt delete mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/NotificationChannel.kt delete mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/BaseEmailChannel.kt delete mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/EmailChannelFactory.kt delete mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/EmailMimeProvider.kt delete mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/SesChannel.kt delete mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/SmtpChannel.kt delete mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/SendMessageRestHandler.kt delete mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/Accountant.kt delete mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/CounterIndex.kt delete mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/CounterIndexModel.kt delete mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/Counters.kt delete mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/EmptyMessageCounter.kt delete mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/MessageCounter.kt delete mode 100644 notifications/notifications/src/test/kotlin/org/opensearch/integtest/NotificationsRestTestCase.kt delete mode 100644 notifications/notifications/src/test/kotlin/org/opensearch/integtest/channel/SesChannelIT.kt delete mode 100644 notifications/notifications/src/test/kotlin/org/opensearch/integtest/channel/SmtpChannelIT.kt delete mode 100644 notifications/notifications/src/test/kotlin/org/opensearch/notifications/resthandler/SendMessageRestHandlerTests.kt create mode 100644 notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSesClient.kt rename notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/{DestinationSNSClient.kt => DestinationSnsClient.kt} (52%) create mode 100644 notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/EmailMessageValidator.kt create mode 100644 notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SesClientFactory.kt rename notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/{SNSClient.kt => SnsClientFactory.kt} (72%) delete mode 100644 notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SNSClientFactory.kt create mode 100644 notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SesClientFactoryImpl.kt create mode 100644 notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SnsClientFactoryImpl.kt create mode 100644 notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SesDestination.kt rename notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/{SNSDestination.kt => SnsDestination.kt} (66%) create mode 100644 notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SesDestinationTransport.kt rename notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/{SNSDestinationTransport.kt => SnsDestinationTransport.kt} (50%) diff --git a/notifications/notifications/build.gradle b/notifications/notifications/build.gradle index 638e5158..f0800b3a 100644 --- a/notifications/notifications/build.gradle +++ b/notifications/notifications/build.gradle @@ -71,17 +71,6 @@ configurations.all { resolutionStrategy { force "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}" force "org.jetbrains.kotlin:kotlin-stdlib-common:${kotlin_version}" - force "commons-logging:commons-logging:1.2" // resolve for awssdk:ses - force "commons-codec:commons-codec:1.13" // resolve for awssdk:ses - force "io.netty:netty-codec-http:4.1.63.Final" // resolve for awssdk:ses - force "io.netty:netty-handler:4.1.63.Final" // resolve for awssdk:ses - force "org.apache.httpcomponents:httpclient:4.5.10" // resolve for awssdk:ses - force "org.apache.httpcomponents:httpcore:4.4.13" // resolve for awssdk:ses - force "com.fasterxml.jackson.core:jackson-core:2.12.3" // resolve for awssdk:ses - force "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.12.3" // resolve for awssdk:ses - force "com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.12.3" // resolve for awssdk:ses - force "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.12.3" // resolve for awssdk:ses - force "junit:junit:4.12" // resolve for awssdk:ses } } @@ -91,9 +80,6 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-common:${kotlin_version}" compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3" // ${kotlin_version} does not work for coroutines compile "${group}:common-utils:${opensearch_version}.0" - compile ("software.amazon.awssdk:ses:2.16.75") { - exclude module: 'annotations' // conflict with org.jetbrains:annotations, integTestRunner fails with error "codebase property already set" - } testImplementation( 'org.assertj:assertj-core:3.19.0', diff --git a/notifications/notifications/src/main/config/notifications.yml b/notifications/notifications/src/main/config/notifications.yml index 0c093145..44759a47 100644 --- a/notifications/notifications/src/main/config/notifications.yml +++ b/notifications/notifications/src/main/config/notifications.yml @@ -31,17 +31,6 @@ opensearch.notifications: general: operationTimeoutMs: 60000 # 60 seconds, Minimum 100ms defaultItemsQueryCount: 100 # default number of items to query - email: - channel: "smtp" # ses or smtp, provide corresponding sections - fromAddress: "from@email.com" - monthlyLimit: 200 - sizeLimit: 10000000 # 10MB Email size limit - ses: # Configuration for Amazon SES email delivery - awsRegion: "us-west-2" - smtp: # Configuration for SMTP email delivery - host: "localhost" - port: 10255 - transportMethod: "starttls" # starttls, ssl or plain access: adminAccess: "All" # adminAccess values: diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationPlugin.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationPlugin.kt index 08902e58..a964c68e 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationPlugin.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationPlugin.kt @@ -50,7 +50,6 @@ import org.opensearch.notifications.action.GetFeatureChannelListAction import org.opensearch.notifications.action.GetNotificationConfigAction import org.opensearch.notifications.action.GetNotificationEventAction import org.opensearch.notifications.action.GetPluginFeaturesAction -import org.opensearch.notifications.action.SendMessageAction import org.opensearch.notifications.action.SendNotificationAction import org.opensearch.notifications.action.UpdateNotificationConfigAction import org.opensearch.notifications.index.ConfigIndexingActions @@ -61,12 +60,10 @@ import org.opensearch.notifications.resthandler.NotificationConfigRestHandler import org.opensearch.notifications.resthandler.NotificationEventRestHandler import org.opensearch.notifications.resthandler.NotificationFeatureChannelListRestHandler import org.opensearch.notifications.resthandler.NotificationFeaturesRestHandler -import org.opensearch.notifications.resthandler.SendMessageRestHandler import org.opensearch.notifications.resthandler.SendTestMessageRestHandler import org.opensearch.notifications.security.UserAccessManager import org.opensearch.notifications.send.SendMessageActionHelper import org.opensearch.notifications.settings.PluginSettings -import org.opensearch.notifications.throttle.Accountant import org.opensearch.plugins.ActionPlugin import org.opensearch.plugins.Plugin import org.opensearch.repositories.RepositoriesService @@ -125,7 +122,6 @@ internal class NotificationPlugin : ActionPlugin, Plugin() { ConfigIndexingActions.initialize(NotificationConfigIndex, UserAccessManager) SendMessageActionHelper.initialize(NotificationConfigIndex, NotificationEventIndex, UserAccessManager) EventIndexingActions.initialize(NotificationEventIndex, UserAccessManager) - Accountant.initialize(client, clusterService) return listOf() } @@ -135,7 +131,6 @@ internal class NotificationPlugin : ActionPlugin, Plugin() { override fun getActions(): List> { log.debug("$LOG_PREFIX:getActions") return listOf( - ActionPlugin.ActionHandler(SendMessageAction.ACTION_TYPE, SendMessageAction::class.java), ActionPlugin.ActionHandler( NotificationsActions.CREATE_NOTIFICATION_CONFIG_ACTION_TYPE, CreateNotificationConfigAction::class.java @@ -185,7 +180,6 @@ internal class NotificationPlugin : ActionPlugin, Plugin() { ): List { log.debug("$LOG_PREFIX:getRestHandlers") return listOf( - SendMessageRestHandler(), NotificationConfigRestHandler(), NotificationEventRestHandler(), NotificationFeaturesRestHandler(), diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/SendMessageAction.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/SendMessageAction.kt deleted file mode 100644 index 6c12820b..00000000 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/SendMessageAction.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.notifications.action - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.runBlocking -import org.opensearch.OpenSearchStatusException -import org.opensearch.action.ActionType -import org.opensearch.action.support.ActionFilters -import org.opensearch.client.Client -import org.opensearch.common.inject.Inject -import org.opensearch.common.xcontent.NamedXContentRegistry -import org.opensearch.commons.authuser.User -import org.opensearch.commons.utils.logger -import org.opensearch.notifications.NotificationPlugin.Companion.LOG_PREFIX -import org.opensearch.notifications.channel.ChannelFactory -import org.opensearch.notifications.model.ChannelMessageResponse -import org.opensearch.notifications.model.SendMessageRequest -import org.opensearch.notifications.model.SendMessageResponse -import org.opensearch.notifications.throttle.Accountant -import org.opensearch.notifications.throttle.Counters -import org.opensearch.rest.RestStatus -import org.opensearch.transport.TransportService - -/** - * Send message action for send notification request. - */ -internal class SendMessageAction @Inject constructor( - transportService: TransportService, - client: Client, - actionFilters: ActionFilters, - val xContentRegistry: NamedXContentRegistry -) : PluginBaseAction( - NAME, - transportService, - client, - actionFilters, - ::SendMessageRequest -) { - companion object { - private const val NAME = "cluster:admin/opensearch/notifications/send" - internal val ACTION_TYPE = ActionType(NAME, ::SendMessageResponse) - private val log by logger(SendMessageAction::class.java) - } - - /** - * {@inheritDoc} - */ - override fun executeRequest(request: SendMessageRequest, user: User?): SendMessageResponse { - log.debug("$LOG_PREFIX:send") - if (!isMessageQuotaAvailable(request)) { - log.info("$LOG_PREFIX:${request.refTag}:Message Sending quota not available") - throw OpenSearchStatusException("Message Sending quota not available", RestStatus.TOO_MANY_REQUESTS) - } - val statusList: List = sendMessagesInParallel(request) - statusList.forEach { - log.info("$LOG_PREFIX:${request.refTag}:statusCode=${it.statusCode}, statusText=${it.statusText}") - } - return SendMessageResponse(request.refTag, statusList) - } - - private fun sendMessagesInParallel(sendMessageRequest: SendMessageRequest): List { - val counters = Counters() - counters.requestCount.incrementAndGet() - val statusList: List - // Fire all the message sending in parallel - runBlocking { - val statusDeferredList = sendMessageRequest.recipients.map { - async(Dispatchers.IO) { sendMessageToChannel(it, sendMessageRequest, counters) } - } - statusList = statusDeferredList.awaitAll() - } - // After all operation are executed, update the counters - Accountant.incrementCounters(counters) - return statusList - } - - private fun sendMessageToChannel( - recipient: String, - sendMessageRequest: SendMessageRequest, - counters: Counters - ): ChannelMessageResponse { - val channel = ChannelFactory.getNotificationChannel(recipient) - return channel.sendMessage( - sendMessageRequest.refTag, - recipient, - sendMessageRequest.title, - sendMessageRequest.channelMessage, - counters - ) - } - - private fun isMessageQuotaAvailable(sendMessageRequest: SendMessageRequest): Boolean { - val counters = Counters() - sendMessageRequest.recipients.forEach { - ChannelFactory.getNotificationChannel(it) - .updateCounter(sendMessageRequest.refTag, it, sendMessageRequest.channelMessage, counters) - } - return Accountant.isMessageQuotaAvailable(counters) - } -} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/ChannelFactory.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/ChannelFactory.kt deleted file mode 100644 index b2d118b2..00000000 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/ChannelFactory.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.notifications.channel - -import org.opensearch.notifications.channel.email.EmailChannelFactory -import org.opensearch.notifications.channel.email.EmailChannelFactory.EMAIL_PREFIX - -/** - * Factory object for creating and providing channel provider. - */ -internal object ChannelFactory : ChannelProvider { - private val channelMap = mapOf(EMAIL_PREFIX to EmailChannelFactory) - - /** - * {@inheritDoc} - */ - override fun getNotificationChannel(recipient: String): NotificationChannel { - var mappedChannel: NotificationChannel = EmptyChannel - if (!recipient.contains(':')) { // if channel info not present - mappedChannel = EmailChannelFactory.getNotificationChannel(recipient) // Default channel is email - } else { - for (it in channelMap) { - if (recipient.startsWith(it.key, true)) { - mappedChannel = it.value.getNotificationChannel(recipient) - break - } - } - } - return mappedChannel - } -} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/ChannelProvider.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/ChannelProvider.kt deleted file mode 100644 index bb688847..00000000 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/ChannelProvider.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.notifications.channel - -/** - * Interface for channel provider for specific recipient depending on its type. - */ -internal interface ChannelProvider { - /** - * gets notification channel for specific recipient depending on its type (prefix). - * @param recipient recipient address to send notification to. prefix with channel type e.g. "mailto:email@address.com" - * @return Notification channel for sending notification for given recipient (depending on its type) - */ - fun getNotificationChannel(recipient: String): NotificationChannel -} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/EmptyChannel.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/EmptyChannel.kt deleted file mode 100644 index cb7ef9fb..00000000 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/EmptyChannel.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.notifications.channel - -import org.opensearch.commons.notifications.model.ChannelMessage -import org.opensearch.notifications.model.ChannelMessageResponse -import org.opensearch.notifications.throttle.Counters -import org.opensearch.rest.RestStatus - -/** - * Empty implementation of the notification channel which responds with error for all requests without any operations. - */ -internal object EmptyChannel : NotificationChannel { - /** - * {@inheritDoc} - */ - override fun sendMessage( - refTag: String, - recipient: String, - title: String, - channelMessage: ChannelMessage, - counter: Counters - ): ChannelMessageResponse { - return ChannelMessageResponse( - recipient, - RestStatus.UNPROCESSABLE_ENTITY, - "No Configured Channel for recipient type:${recipient.substringBefore(':', "empty")}" - ) - } - - /** - * {@inheritDoc} - */ - override fun updateCounter(refTag: String, recipient: String, channelMessage: ChannelMessage, counter: Counters) { - return - } -} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/NotificationChannel.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/NotificationChannel.kt deleted file mode 100644 index 4ef6b66b..00000000 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/NotificationChannel.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.notifications.channel - -import org.opensearch.commons.notifications.model.ChannelMessage -import org.opensearch.notifications.model.ChannelMessageResponse -import org.opensearch.notifications.throttle.Counters - -/** - * Interface for sending notification message over a implemented channel. - */ -internal interface NotificationChannel { - - /** - * Update the counter if the notification message is over this channel. Do not actually send message. - * Used for checking message quotas. - * - * @param refTag ref tag for logging purpose - * @param recipient recipient address to send notification to - * @param channelMessage The message to send notification - * @param counter The counter object to update the detail for accounting purpose - */ - fun updateCounter(refTag: String, recipient: String, channelMessage: ChannelMessage, counter: Counters) - - /** - * Sending notification message over this channel. - * - * @param refTag ref tag for logging purpose - * @param recipient recipient address to send notification to - * @param title The title to send notification - * @param channelMessage The message to send notification - * @param counter The counter object to update the detail for accounting purpose - * @return Channel message response - */ - fun sendMessage( - refTag: String, - recipient: String, - title: String, - channelMessage: ChannelMessage, - counter: Counters - ): ChannelMessageResponse -} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/BaseEmailChannel.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/BaseEmailChannel.kt deleted file mode 100644 index 66e872f7..00000000 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/BaseEmailChannel.kt +++ /dev/null @@ -1,167 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.notifications.channel.email - -import org.opensearch.commons.notifications.model.Attachment -import org.opensearch.commons.notifications.model.ChannelMessage -import org.opensearch.notifications.channel.NotificationChannel -import org.opensearch.notifications.model.ChannelMessageResponse -import org.opensearch.notifications.settings.PluginSettings -import org.opensearch.notifications.throttle.Counters -import org.opensearch.rest.RestStatus -import java.io.IOException -import javax.mail.MessagingException -import javax.mail.Session -import javax.mail.internet.AddressException -import javax.mail.internet.MimeMessage - -/** - * Notification channel for sending mail to Email server. - */ -internal abstract class BaseEmailChannel : NotificationChannel { - - companion object { - private const val MINIMUM_EMAIL_HEADER_LENGTH = 160 // minimum value from 100 reference emails - } - - /** - * {@inheritDoc} - */ - override fun updateCounter(refTag: String, recipient: String, channelMessage: ChannelMessage, counter: Counters) { - counter.emailSentSuccessCount.incrementAndGet() - } - - /** - * {@inheritDoc} - */ - override fun sendMessage( - refTag: String, - recipient: String, - title: String, - channelMessage: ChannelMessage, - counter: Counters - ): ChannelMessageResponse { - val retStatus = sendEmail(refTag, recipient, title, channelMessage) - if (retStatus.statusCode == RestStatus.OK) { - counter.emailSentSuccessCount.incrementAndGet() - } else { - counter.emailSentFailureCount.incrementAndGet() - } - return retStatus - } - - /** - * Sending Email message to server. - * @param refTag ref tag for logging purpose - * @param recipient email recipient to send mail to - * @param title email subject to send - * @param channelMessage email message information to compose email - * @return Channel message response - */ - private fun sendEmail( - refTag: String, - recipient: String, - title: String, - channelMessage: ChannelMessage - ): ChannelMessageResponse { - val fromAddress = PluginSettings.emailFromAddress - if (PluginSettings.UNCONFIGURED_EMAIL_ADDRESS == fromAddress) { - return ChannelMessageResponse(recipient, RestStatus.NOT_IMPLEMENTED, "Email from: address not configured") - } - if (isMessageSizeOverLimit(title, channelMessage)) { - return ChannelMessageResponse( - recipient, - RestStatus.REQUEST_ENTITY_TOO_LARGE, - "Email size larger than ${PluginSettings.emailSizeLimit}" - ) - } - val mimeMessage: MimeMessage - return try { - val session = prepareSession(refTag, recipient, channelMessage) - mimeMessage = EmailMimeProvider.prepareMimeMessage(session, fromAddress, recipient, title, channelMessage) - sendMimeMessage(refTag, recipient, mimeMessage) - } catch (addressException: AddressException) { - ChannelMessageResponse( - recipient, - RestStatus.BAD_REQUEST, - "recipient parsing failed with status:${addressException.message}" - ) - } catch (messagingException: MessagingException) { - ChannelMessageResponse( - recipient, - RestStatus.FAILED_DEPENDENCY, - "Email message creation failed with status:${messagingException.message}" - ) - } catch (ioException: IOException) { - ChannelMessageResponse( - recipient, - RestStatus.FAILED_DEPENDENCY, - "Email message creation failed with status:${ioException.message}" - ) - } - } - - private fun isMessageSizeOverLimit(title: String, channelMessage: ChannelMessage): Boolean { - val attachment: Attachment? = channelMessage.attachment - val approxAttachmentLength = if (attachment != null) { - MINIMUM_EMAIL_HEADER_LENGTH + - attachment.fileData.length + - attachment.fileName.length - } else { - 0 - } - val approxEmailLength = MINIMUM_EMAIL_HEADER_LENGTH + - title.length + - channelMessage.textDescription.length + - (channelMessage.htmlDescription?.length ?: 0) + - approxAttachmentLength - return approxEmailLength > PluginSettings.emailSizeLimit - } - - /** - * Prepare Session for creating Email mime message. - * @param refTag ref tag for logging purpose - * @param recipient email recipient to send mail to - * @param channelMessage email message information to compose email - * @return initialized/prepared Session for creating mime message - */ - protected abstract fun prepareSession(refTag: String, recipient: String, channelMessage: ChannelMessage): Session - - /** - * Sending Email mime message to server. - * @param refTag ref tag for logging purpose - * @param recipient email recipient to send mail to - * @param mimeMessage mime message to send to Email server - * @return Channel message response - */ - protected abstract fun sendMimeMessage( - refTag: String, - recipient: String, - mimeMessage: MimeMessage - ): ChannelMessageResponse -} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/EmailChannelFactory.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/EmailChannelFactory.kt deleted file mode 100644 index d01ebbd4..00000000 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/EmailChannelFactory.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.notifications.channel.email - -import org.opensearch.notifications.channel.ChannelProvider -import org.opensearch.notifications.channel.EmptyChannel -import org.opensearch.notifications.channel.NotificationChannel -import org.opensearch.notifications.settings.EmailChannelType -import org.opensearch.notifications.settings.PluginSettings - -/** - * Factory object for creating and providing email channel provider. - */ -internal object EmailChannelFactory : ChannelProvider { - const val EMAIL_PREFIX = "mailto:" - private val channelMap: Map = mapOf( - EmailChannelType.SMTP.stringValue to SmtpChannel, - EmailChannelType.SES.stringValue to SesChannel - ) - - /** - * {@inheritDoc} - */ - override fun getNotificationChannel(recipient: String): NotificationChannel { - return channelMap.getOrDefault(PluginSettings.emailChannel, EmptyChannel) - } -} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/EmailMimeProvider.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/EmailMimeProvider.kt deleted file mode 100644 index ba275d43..00000000 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/EmailMimeProvider.kt +++ /dev/null @@ -1,159 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.notifications.channel.email - -import org.opensearch.commons.notifications.model.Attachment -import org.opensearch.commons.notifications.model.ChannelMessage -import java.util.Base64 -import javax.activation.DataHandler -import javax.mail.Message -import javax.mail.Session -import javax.mail.internet.MimeBodyPart -import javax.mail.internet.MimeMessage -import javax.mail.internet.MimeMultipart -import javax.mail.util.ByteArrayDataSource - -/** - * Object for creating mime message from the channel message for sending mail. - */ -internal object EmailMimeProvider { - /** - * Create and prepare mime message to send mail - * @param session The mail session to use to create mime message - * @param fromAddress "From:" address of the email message - * @param recipient "To:" address of the email message - * @param title The title to send notification - * @param channelMessage The message to send notification - * @return The created and prepared mime message object - */ - fun prepareMimeMessage( - session: Session, - fromAddress: String, - recipient: String, - title: String, - channelMessage: ChannelMessage - ): MimeMessage { - // Create a new MimeMessage object - val message = MimeMessage(session) - - // Add from: - message.setFrom(extractEmail(fromAddress)) - - // Add to: - message.setRecipients(Message.RecipientType.TO, extractEmail(recipient)) - - // Add Subject: - message.setSubject(title, "UTF-8") - - // Create a multipart/alternative child container - val msgBody = MimeMultipart("alternative") - - // Create a wrapper for the HTML and text parts - val bodyWrapper = MimeBodyPart() - - // Define the text part (if html part does not exists then use "-" string - val textPart = MimeBodyPart() - textPart.setContent(channelMessage.textDescription, "text/plain; charset=UTF-8") - // Add the text part to the child container - msgBody.addBodyPart(textPart) - - // Define the HTML part - if (channelMessage.htmlDescription != null) { - val htmlPart = MimeBodyPart() - htmlPart.setContent(channelMessage.htmlDescription, "text/html; charset=UTF-8") - // Add the HTML part to the child container - msgBody.addBodyPart(htmlPart) - } - // Add the child container to the wrapper object - bodyWrapper.setContent(msgBody) - - // Create a multipart/mixed parent container - val msg = MimeMultipart("mixed") - - // Add the parent container to the message - message.setContent(msg) - - // Add the multipart/alternative part to the message - msg.addBodyPart(bodyWrapper) - - val attachment: Attachment? = channelMessage.attachment - if (attachment != null) { - // Add the attachment to the message - var attachmentMime: MimeBodyPart? = null - when (attachment.fileEncoding) { - "text" -> attachmentMime = createTextAttachmentPart(attachment) - "base64" -> attachmentMime = createBinaryAttachmentPart(attachment) - } - if (attachmentMime != null) { - msg.addBodyPart(attachmentMime) - } - } - return message - } - - /** - * Extract email address from "mailto:email@address.com" format - * @param recipient input email address - * @return extracted email address - */ - private fun extractEmail(recipient: String): String { - if (recipient.startsWith(EmailChannelFactory.EMAIL_PREFIX)) { - return recipient.drop(EmailChannelFactory.EMAIL_PREFIX.length) - } - return recipient - } - - /** - * Create a binary attachment part from channel attachment message - * @param attachment channel attachment message - * @return created mime body part for binary attachment - */ - private fun createBinaryAttachmentPart(attachment: Attachment): MimeBodyPart { - val attachmentMime = MimeBodyPart() - val fds = ByteArrayDataSource( - Base64.getMimeDecoder().decode(attachment.fileData), - attachment.fileContentType ?: "application/octet-stream" - ) - attachmentMime.dataHandler = DataHandler(fds) - attachmentMime.fileName = attachment.fileName - return attachmentMime - } - - /** - * Create a text attachment part from channel attachment message - * @param attachment channel attachment message - * @return created mime body part for text attachment - */ - private fun createTextAttachmentPart(attachment: Attachment): MimeBodyPart { - val attachmentMime = MimeBodyPart() - val subContentType = attachment.fileContentType?.substringAfterLast('/') ?: "plain" - attachmentMime.setText(attachment.fileData, "UTF-8", subContentType) - attachmentMime.fileName = attachment.fileName - return attachmentMime - } -} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/SesChannel.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/SesChannel.kt deleted file mode 100644 index 2bf66978..00000000 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/SesChannel.kt +++ /dev/null @@ -1,138 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.notifications.channel.email - -import org.opensearch.commons.notifications.model.ChannelMessage -import org.opensearch.commons.utils.logger -import org.opensearch.notifications.NotificationPlugin.Companion.LOG_PREFIX -import org.opensearch.notifications.model.ChannelMessageResponse -import org.opensearch.notifications.settings.PluginSettings -import org.opensearch.notifications.spi.utils.SecurityAccess -import org.opensearch.rest.RestStatus -import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider -import software.amazon.awssdk.core.SdkBytes -import software.amazon.awssdk.core.exception.SdkException -import software.amazon.awssdk.regions.Region -import software.amazon.awssdk.services.ses.SesClient -import software.amazon.awssdk.services.ses.model.AccountSendingPausedException -import software.amazon.awssdk.services.ses.model.ConfigurationSetDoesNotExistException -import software.amazon.awssdk.services.ses.model.ConfigurationSetSendingPausedException -import software.amazon.awssdk.services.ses.model.MailFromDomainNotVerifiedException -import software.amazon.awssdk.services.ses.model.MessageRejectedException -import software.amazon.awssdk.services.ses.model.RawMessage -import software.amazon.awssdk.services.ses.model.SendRawEmailRequest -import software.amazon.awssdk.services.ses.model.SesException -import java.io.ByteArrayOutputStream -import java.util.Properties -import javax.mail.Session -import javax.mail.internet.MimeMessage - -/** - * Notification channel for sending mail over Amazon SES. - */ -internal object SesChannel : BaseEmailChannel() { - private val log by logger(javaClass) - - /** - * {@inheritDoc} - */ - override fun prepareSession(refTag: String, recipient: String, channelMessage: ChannelMessage): Session { - val prop = Properties() - prop["mail.transport.protocol"] = "smtp" - return Session.getInstance(prop) - } - - /** - * {@inheritDoc} - */ - override fun sendMimeMessage(refTag: String, recipient: String, mimeMessage: MimeMessage): ChannelMessageResponse { - return try { - log.debug("$LOG_PREFIX:Sending Email-SES:$refTag") - val region = Region.of(PluginSettings.sesAwsRegion) - val client = SecurityAccess.doPrivileged { - SesClient.builder().region(region).credentialsProvider(DefaultCredentialsProvider.create()).build() - } - val outputStream = ByteArrayOutputStream() - SecurityAccess.doPrivileged { mimeMessage.writeTo(outputStream) } - val emailSize = outputStream.size() - if (emailSize <= PluginSettings.emailSizeLimit) { - val data = SdkBytes.fromByteArray(outputStream.toByteArray()) - val rawMessage = RawMessage.builder() - .data(data) - .build() - val rawEmailRequest = SendRawEmailRequest.builder() - .rawMessage(rawMessage) - .build() - val response = SecurityAccess.doPrivileged { client.sendRawEmail(rawEmailRequest) } - log.info("$LOG_PREFIX:Email-SES:$refTag status:$response") - ChannelMessageResponse(recipient, RestStatus.OK, "Success") - } else { - ChannelMessageResponse( - recipient, - RestStatus.REQUEST_ENTITY_TOO_LARGE, - "Email size($emailSize) larger than ${PluginSettings.emailSizeLimit}" - ) - } - } catch (exception: MessageRejectedException) { - ChannelMessageResponse(recipient, RestStatus.SERVICE_UNAVAILABLE, getSesExceptionText(exception)) - } catch (exception: MailFromDomainNotVerifiedException) { - ChannelMessageResponse(recipient, RestStatus.FORBIDDEN, getSesExceptionText(exception)) - } catch (exception: ConfigurationSetDoesNotExistException) { - ChannelMessageResponse(recipient, RestStatus.NOT_IMPLEMENTED, getSesExceptionText(exception)) - } catch (exception: ConfigurationSetSendingPausedException) { - ChannelMessageResponse(recipient, RestStatus.SERVICE_UNAVAILABLE, getSesExceptionText(exception)) - } catch (exception: AccountSendingPausedException) { - ChannelMessageResponse(recipient, RestStatus.INSUFFICIENT_STORAGE, getSesExceptionText(exception)) - } catch (exception: SesException) { - ChannelMessageResponse(recipient, RestStatus.FAILED_DEPENDENCY, getSesExceptionText(exception)) - } catch (exception: SdkException) { - ChannelMessageResponse(recipient, RestStatus.FAILED_DEPENDENCY, getSdkExceptionText(exception)) - } - } - - /** - * Create error string from Amazon SES Exceptions - * @param exception SES Exception - * @return generated error string - */ - private fun getSesExceptionText(exception: SesException): String { - val httpResponse = exception.awsErrorDetails().sdkHttpResponse() - log.info("$LOG_PREFIX:SesException $exception") - return "sendEmail Error, SES status:${httpResponse.statusCode()}:${httpResponse.statusText()}" - } - - /** - * Create error string from Amazon SDK Exceptions - * @param exception SDK Exception - * @return generated error string - */ - private fun getSdkExceptionText(exception: SdkException): String { - log.info("$LOG_PREFIX:SdkException $exception") - return "sendEmail Error, SDK status:${exception.message}" - } -} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/SmtpChannel.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/SmtpChannel.kt deleted file mode 100644 index 4662c2aa..00000000 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/channel/email/SmtpChannel.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.notifications.channel.email - -import com.sun.mail.util.MailConnectException -import org.opensearch.commons.notifications.model.ChannelMessage -import org.opensearch.commons.utils.logger -import org.opensearch.notifications.NotificationPlugin.Companion.LOG_PREFIX -import org.opensearch.notifications.model.ChannelMessageResponse -import org.opensearch.notifications.settings.PluginSettings -import org.opensearch.notifications.spi.utils.SecurityAccess -import org.opensearch.rest.RestStatus -import java.util.Properties -import javax.mail.MessagingException -import javax.mail.SendFailedException -import javax.mail.Session -import javax.mail.Transport -import javax.mail.internet.MimeMessage - -/** - * Notification channel for sending mail over SMTP server. - */ -internal object SmtpChannel : BaseEmailChannel() { - private val log by logger(javaClass) - - /** - * {@inheritDoc} - */ - override fun prepareSession(refTag: String, recipient: String, channelMessage: ChannelMessage): Session { - val prop = Properties() - prop["mail.transport.protocol"] = "smtp" - prop["mail.smtp.host"] = PluginSettings.smtpHost - prop["mail.smtp.port"] = PluginSettings.smtpPort - when (PluginSettings.smtpTransportMethod) { - "ssl" -> prop["mail.smtp.ssl.enable"] = true - "starttls" -> prop["mail.smtp.starttls.enable"] = true - } - return Session.getInstance(prop) - } - - /** - * {@inheritDoc} - */ - override fun sendMimeMessage(refTag: String, recipient: String, mimeMessage: MimeMessage): ChannelMessageResponse { - return try { - log.debug("$LOG_PREFIX:Sending Email-SMTP:$refTag") - SecurityAccess.doPrivileged { Transport.send(mimeMessage) } - log.info("$LOG_PREFIX:Email-SMTP:$refTag sent") - ChannelMessageResponse(recipient, RestStatus.OK, "Success") - } catch (exception: SendFailedException) { - ChannelMessageResponse(recipient, RestStatus.BAD_GATEWAY, getMessagingExceptionText(exception)) - } catch (exception: MailConnectException) { - ChannelMessageResponse(recipient, RestStatus.SERVICE_UNAVAILABLE, getMessagingExceptionText(exception)) - } catch (exception: MessagingException) { - ChannelMessageResponse(recipient, RestStatus.FAILED_DEPENDENCY, getMessagingExceptionText(exception)) - } - } - - /** - * Create error string from MessagingException - * @param exception Messaging Exception - * @return generated error string - */ - private fun getMessagingExceptionText(exception: MessagingException): String { - log.info("$LOG_PREFIX:EmailException $exception") - return "sendEmail Error, status:${exception.message}" - } -} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt index 51ac6ebf..b1bc77df 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt @@ -49,6 +49,7 @@ import org.opensearch.commons.notifications.model.FeatureChannelList import org.opensearch.commons.notifications.model.NotificationConfig import org.opensearch.commons.notifications.model.NotificationConfigInfo import org.opensearch.commons.notifications.model.NotificationConfigSearchResult +import org.opensearch.commons.notifications.model.SNS import org.opensearch.commons.notifications.model.Slack import org.opensearch.commons.notifications.model.SmtpAccount import org.opensearch.commons.notifications.model.Webhook @@ -91,6 +92,11 @@ object ConfigIndexingActions { // TODO: URL validation with rules } + @Suppress("UnusedPrivateMember") + private fun validateSnsConfig(sns: SNS, user: User?) { + // TODO: URL validation with rules + } + private fun validateEmailConfig(email: Email, features: EnumSet, user: User?) { if (email.emailGroupIds.contains(email.emailAccountID)) { throw OpenSearchStatusException( @@ -176,6 +182,7 @@ object ConfigIndexingActions { ConfigType.EMAIL -> validateEmailConfig(config.configData as Email, config.features, user) ConfigType.SMTP_ACCOUNT -> validateSmtpAccountConfig(config.configData as SmtpAccount, user) ConfigType.EMAIL_GROUP -> validateEmailGroupConfig(config.configData as EmailGroup, user) + ConfigType.SNS -> validateSnsConfig(config.configData as SNS, user) } } diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigQueryHelper.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigQueryHelper.kt index 13df647b..7aeb5524 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigQueryHelper.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigQueryHelper.kt @@ -41,6 +41,8 @@ import org.opensearch.commons.notifications.NotificationConstants.METHOD_TAG import org.opensearch.commons.notifications.NotificationConstants.NAME_TAG import org.opensearch.commons.notifications.NotificationConstants.QUERY_TAG import org.opensearch.commons.notifications.NotificationConstants.RECIPIENT_LIST_TAG +import org.opensearch.commons.notifications.NotificationConstants.ROLE_ARN_FIELD +import org.opensearch.commons.notifications.NotificationConstants.TOPIC_ARN_FIELD import org.opensearch.commons.notifications.NotificationConstants.UPDATED_TIME_TAG import org.opensearch.commons.notifications.NotificationConstants.URL_TAG import org.opensearch.commons.notifications.model.ConfigType.CHIME @@ -48,6 +50,7 @@ import org.opensearch.commons.notifications.model.ConfigType.EMAIL import org.opensearch.commons.notifications.model.ConfigType.EMAIL_GROUP import org.opensearch.commons.notifications.model.ConfigType.SLACK import org.opensearch.commons.notifications.model.ConfigType.SMTP_ACCOUNT +import org.opensearch.commons.notifications.model.ConfigType.SNS import org.opensearch.commons.notifications.model.ConfigType.WEBHOOK import org.opensearch.index.query.BoolQueryBuilder import org.opensearch.index.query.QueryBuilder @@ -84,7 +87,9 @@ object ConfigQueryHelper { "${EMAIL.tag}.$RECIPIENT_LIST_TAG", "${SMTP_ACCOUNT.tag}.$HOST_TAG", "${SMTP_ACCOUNT.tag}.$FROM_ADDRESS_TAG", - "${EMAIL_GROUP.tag}.$RECIPIENT_LIST_TAG" + "${EMAIL_GROUP.tag}.$RECIPIENT_LIST_TAG", + "${SNS.tag}.$TOPIC_ARN_FIELD", + "${SNS.tag}.$ROLE_ARN_FIELD" ) private val METADATA_FIELDS = METADATA_RANGE_FIELDS diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationConfigRestHandler.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationConfigRestHandler.kt index ec214bc0..5a8ac03b 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationConfigRestHandler.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationConfigRestHandler.kt @@ -143,6 +143,8 @@ internal class NotificationConfigRestHandler : PluginBaseHandler() { * smtp_account.host=domain * smtp_account.from_address=abc,xyz * smtp_account.recipient_list=abc,xyz + * sns.topic_arn=abc,xyz + * sns.role_arn=abc,xyz * query=search all above fields * Request body: Ref [org.opensearch.commons.notifications.action.GetNotificationConfigRequest] * Response body: [org.opensearch.commons.notifications.action.GetNotificationConfigResponse] diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/SendMessageRestHandler.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/SendMessageRestHandler.kt deleted file mode 100644 index a4aaa4c4..00000000 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/SendMessageRestHandler.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.notifications.resthandler - -import org.opensearch.client.node.NodeClient -import org.opensearch.commons.utils.contentParserNextToken -import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI -import org.opensearch.notifications.action.SendMessageAction -import org.opensearch.notifications.model.SendMessageRequest -import org.opensearch.rest.BaseRestHandler -import org.opensearch.rest.BaseRestHandler.RestChannelConsumer -import org.opensearch.rest.BytesRestResponse -import org.opensearch.rest.RestHandler.Route -import org.opensearch.rest.RestRequest -import org.opensearch.rest.RestRequest.Method.POST -import org.opensearch.rest.RestStatus - -/** - * Rest handler for sending notification. - * This handler [SendAction] for sending notification. - */ -internal class SendMessageRestHandler : BaseRestHandler() { - - internal companion object { - const val SEND_BASE_URI = "$PLUGIN_BASE_URI/send" - } - - /** - * {@inheritDoc} - */ - override fun getName(): String = "send_message" - - /** - * {@inheritDoc} - */ - override fun routes(): List { - return listOf( - Route(POST, SEND_BASE_URI) - ) - } - - /** - * {@inheritDoc} - */ - override fun responseParams(): Set { - return setOf() - } - - /** - * {@inheritDoc} - */ - override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { - return when (request.method()) { - POST -> RestChannelConsumer { - client.execute( - SendMessageAction.ACTION_TYPE, - SendMessageRequest(request.contentParserNextToken()), - RestResponseToXContentListener(it) - ) - } - else -> RestChannelConsumer { - it.sendResponse(BytesRestResponse(RestStatus.METHOD_NOT_ALLOWED, "${request.method()} is not allowed")) - } - } - } -} 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 be22ce9d..559032ac 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 @@ -46,9 +46,9 @@ 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.SNSDestination import org.opensearch.notifications.spi.model.destination.SlackDestination import org.opensearch.notifications.spi.model.destination.SmtpDestination +import org.opensearch.notifications.spi.model.destination.SnsDestination import org.opensearch.rest.RestStatus import java.time.Instant @@ -159,7 +159,8 @@ object SendMessageActionHelper { val configData = channel.configDoc.config.configData var emailRecipientStatus = listOf() if (configType == ConfigType.EMAIL) { - emailRecipientStatus = listOf(EmailRecipientStatus("placeholder@amazon.com", DeliveryStatus("Scheduled", "Pending execution"))) + emailRecipientStatus = + listOf(EmailRecipientStatus("placeholder@amazon.com", DeliveryStatus("Scheduled", "Pending execution"))) } val eventStatus = EventStatus( channel.docInfo.id!!, // ID from query so not expected to be null @@ -175,14 +176,24 @@ object SendMessageActionHelper { val response = when (configType) { ConfigType.NONE -> null - ConfigType.SLACK -> sendSlackMessage(configData as Slack, message, eventStatus) - ConfigType.CHIME -> sendChimeMessage(configData as Chime, message, eventStatus) - ConfigType.WEBHOOK -> sendWebhookMessage(configData as Webhook, message, eventStatus) - ConfigType.EMAIL -> sendEmailMessage(configData as Email, childConfigs, message, eventStatus) - ConfigType.SNS -> sendSNSMessage(configData as SNS, message, eventStatus) + 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.EMAIL -> sendEmailMessage( + configData as Email, + childConfigs, + message, + eventStatus, + eventSource.referenceId + ) ConfigType.SMTP_ACCOUNT -> null ConfigType.EMAIL_GROUP -> null - ConfigType.SNS -> null + ConfigType.SNS -> sendSNSMessage(configData as SNS, message, eventStatus, eventSource.referenceId) } return if (response == null) { log.warn("Cannot send message to destination for config id :${channel.docInfo.id}") @@ -214,27 +225,42 @@ object SendMessageActionHelper { /** * send message to slack destination */ - private fun sendSlackMessage(slack: Slack, message: MessageContent, eventStatus: EventStatus): EventStatus { + private fun sendSlackMessage( + slack: Slack, + message: MessageContent, + eventStatus: EventStatus, + referenceId: String + ): EventStatus { val destination = SlackDestination(slack.url) - val status = sendMessageThroughSpi(destination, message) + val status = sendMessageThroughSpi(destination, message, referenceId) return eventStatus.copy(deliveryStatus = DeliveryStatus(status.statusCode.toString(), status.statusText)) } /** * send message to chime destination */ - private fun sendChimeMessage(chime: Chime, message: MessageContent, eventStatus: EventStatus): EventStatus { + private fun sendChimeMessage( + chime: Chime, + message: MessageContent, + eventStatus: EventStatus, + referenceId: String + ): EventStatus { val destination = ChimeDestination(chime.url) - val status = sendMessageThroughSpi(destination, message) + val status = sendMessageThroughSpi(destination, message, referenceId) return eventStatus.copy(deliveryStatus = DeliveryStatus(status.statusCode.toString(), status.statusText)) } /** * send message to custom webhook destination */ - private fun sendWebhookMessage(webhook: Webhook, message: MessageContent, eventStatus: EventStatus): EventStatus { + private fun sendWebhookMessage( + webhook: Webhook, + message: MessageContent, + eventStatus: EventStatus, + referenceId: String + ): EventStatus { val destination = CustomWebhookDestination(webhook.url, webhook.headerParams, webhook.method.tag) - val status = sendMessageThroughSpi(destination, message) + val status = sendMessageThroughSpi(destination, message, referenceId) return eventStatus.copy(deliveryStatus = DeliveryStatus(status.statusCode.toString(), status.statusText)) } @@ -245,7 +271,8 @@ object SendMessageActionHelper { email: Email, childConfigs: List, message: MessageContent, - eventStatus: EventStatus + eventStatus: EventStatus, + referenceId: String ): EventStatus { val smtpAccountDocInfo = childConfigs.find { it.docInfo.id == email.emailAccountID } val groups = childConfigs.filter { email.emailGroupIds.contains(it.docInfo.id) } @@ -256,7 +283,13 @@ object SendMessageActionHelper { runBlocking { val statusDeferredList = recipients.map { async(Dispatchers.IO) { - sendEmailFromSmtpAccount(smtpAccountConfig.name, smtpAccountConfig.configData as SmtpAccount, it, message) + sendEmailFromSmtpAccount( + smtpAccountConfig.name, + smtpAccountConfig.configData as SmtpAccount, + it, + message, + referenceId + ) } } emailRecipientStatus = statusDeferredList.awaitAll() @@ -286,7 +319,8 @@ object SendMessageActionHelper { accountName: String, smtpAccount: SmtpAccount, recipient: String, - message: MessageContent + message: MessageContent, + referenceId: String ): EmailRecipientStatus { val destination = SmtpDestination( accountName, @@ -296,7 +330,7 @@ object SendMessageActionHelper { smtpAccount.fromAddress, recipient ) - val status = sendMessageThroughSpi(destination, message) + val status = sendMessageThroughSpi(destination, message, referenceId) return EmailRecipientStatus( recipient, DeliveryStatus(status.statusCode.toString(), status.statusText) @@ -306,9 +340,14 @@ object SendMessageActionHelper { /** * send message to SNS destination */ - private fun sendSNSMessage(sns: SNS, message: MessageContent, eventStatus: EventStatus): EventStatus { - val destination = SNSDestination(sns.topicARN, sns.roleARN) - val status = sendMessageThroughSpi(destination, message) + private fun sendSNSMessage( + sns: SNS, + message: MessageContent, + eventStatus: EventStatus, + referenceId: String + ): EventStatus { + val destination = SnsDestination(sns.topicARN, sns.roleARN) + val status = sendMessageThroughSpi(destination, message, referenceId) return eventStatus.copy(deliveryStatus = DeliveryStatus(status.statusCode.toString(), status.statusText)) } @@ -318,10 +357,11 @@ object SendMessageActionHelper { @Suppress("TooGenericExceptionCaught", "UnusedPrivateMember") private fun sendMessageThroughSpi( destination: BaseDestination, - message: MessageContent + message: MessageContent, + referenceId: String ): DestinationMessageResponse { return try { - val status = NotificationSpi.sendMessage(destination, message) + val status = NotificationSpi.sendMessage(destination, message, referenceId) log.info("$LOG_PREFIX:sendMessage:statusCode=${status.statusCode}, statusText=${status.statusText}") status } catch (exception: Exception) { diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/settings/PluginSettings.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/settings/PluginSettings.kt index c9fb5805..78f88908 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/settings/PluginSettings.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/settings/PluginSettings.kt @@ -36,7 +36,6 @@ import org.opensearch.common.settings.Settings import org.opensearch.commons.utils.logger import org.opensearch.notifications.NotificationPlugin.Companion.LOG_PREFIX import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_NAME -import software.amazon.awssdk.regions.Region import java.io.IOException import java.nio.file.Path @@ -46,7 +45,7 @@ import java.nio.file.Path internal object PluginSettings { /** - * Settings Key prefix for this plugin. + * Settings Key-prefix for this plugin. */ private const val KEY_PREFIX = "opensearch.notifications" @@ -55,11 +54,6 @@ internal object PluginSettings { */ private const val GENERAL_KEY_PREFIX = "$KEY_PREFIX.general" - /** - * Settings Key prefix for this plugin. - */ - private const val EMAIL_KEY_PREFIX = "$KEY_PREFIX.email" - /** * Access settings Key prefix. */ @@ -75,46 +69,6 @@ internal object PluginSettings { */ private const val DEFAULT_ITEMS_QUERY_COUNT_KEY = "$GENERAL_KEY_PREFIX.defaultItemsQueryCount" - /** - * Setting to choose smtp or SES for sending mail. - */ - private const val EMAIL_CHANNEL_KEY = "$EMAIL_KEY_PREFIX.channel" - - /** - * "From:" email address while sending email. - */ - private const val EMAIL_FROM_ADDRESS_KEY = "$EMAIL_KEY_PREFIX.fromAddress" - - /** - * Monthly email sending limit from this plugin. - */ - private const val EMAIL_LIMIT_MONTHLY_KEY = "$EMAIL_KEY_PREFIX.monthlyLimit" - - /** - * Email size limit. - */ - private const val EMAIL_SIZE_LIMIT_KEY = "$EMAIL_KEY_PREFIX.sizeLimit" - - /** - * Amazon SES AWS region to send mail to. - */ - private const val EMAIL_SES_AWS_REGION_KEY = "$EMAIL_KEY_PREFIX.ses.awsRegion" - - /** - * SMTP host address to send mail to. - */ - private const val EMAIL_SMTP_HOST_KEY = "$EMAIL_KEY_PREFIX.smtp.host" - - /** - * SMTP port number to send mail to. - */ - private const val EMAIL_SMTP_PORT_KEY = "$EMAIL_KEY_PREFIX.smtp.port" - - /** - * SMTP Transport method. starttls, ssl or plain. - */ - private const val EMAIL_SMTP_TRANSPORT_METHOD_KEY = "$EMAIL_KEY_PREFIX.smtp.transportMethod" - /** * Setting to choose admin access restriction. */ @@ -150,53 +104,6 @@ internal object PluginSettings { */ private const val MINIMUM_ITEMS_QUERY_COUNT = 10 - /** - * Default email channel. - */ - private val DEFAULT_EMAIL_CHANNEL = EmailChannelType.SMTP.stringValue - - /** - * Default monthly email sending limit from this plugin. - */ - private const val DEFAULT_EMAIL_LIMIT_MONTHLY = 200 - - /** - * Default email size limit as 10MB. - */ - private const val DEFAULT_EMAIL_SIZE_LIMIT = 10000000 - - /** - * Minimum email size limit as 10KB. - */ - private const val MINIMUM_EMAIL_SIZE_LIMIT = 10000 - - /** - * Default Amazon SES AWS region. - */ - private val DEFAULT_SES_AWS_REGION = Region.US_WEST_2.id() - - /** - * Default SMTP Host name to connect to. - */ - private const val DEFAULT_SMTP_HOST = "localhost" - - /** - * Default SMTP port number to connect to. - */ - private const val DEFAULT_SMTP_PORT = 10255 - - /** - * Default SMTP transport method. - */ - private const val DEFAULT_SMTP_TRANSPORT_METHOD = "starttls" - - /** - * If the "From:" email address is set to below value then email will NOT be submitted to server. - * any other valid "From:" email address would be submitted to server. - */ - const val UNCONFIGURED_EMAIL_ADDRESS = - "nobody@email.com" // Email will not be sent if email address different than this value - /** * Default admin access method. */ @@ -229,54 +136,6 @@ internal object PluginSettings { @Volatile var defaultItemsQueryCount: Int - /** - * Email channel setting [EmailChannelType] in string format - */ - @Volatile - var emailChannel: String - - /** - * Email "From:" Address setting - */ - @Volatile - var emailFromAddress: String - - /** - * Email monthly throttle limit setting - */ - @Volatile - var emailMonthlyLimit: Int - - /** - * Email size limit setting - */ - @Volatile - var emailSizeLimit: Int - - /** - * Amazon SES AWS region setting - */ - @Volatile - var sesAwsRegion: String - - /** - * SMTP server host setting - */ - @Volatile - var smtpHost: String - - /** - * SMTP server port setting - */ - @Volatile - var smtpPort: Int - - /** - * SMTP server transport method setting - */ - @Volatile - var smtpTransportMethod: String - /** * admin access method. */ @@ -331,14 +190,6 @@ internal object PluginSettings { operationTimeoutMs = (settings?.get(OPERATION_TIMEOUT_MS_KEY)?.toLong()) ?: DEFAULT_OPERATION_TIMEOUT_MS defaultItemsQueryCount = (settings?.get(DEFAULT_ITEMS_QUERY_COUNT_KEY)?.toInt()) ?: DEFAULT_ITEMS_QUERY_COUNT_VALUE - emailChannel = (settings?.get(EMAIL_CHANNEL_KEY) ?: DEFAULT_EMAIL_CHANNEL) - emailFromAddress = (settings?.get(EMAIL_FROM_ADDRESS_KEY) ?: UNCONFIGURED_EMAIL_ADDRESS) - emailMonthlyLimit = (settings?.get(EMAIL_LIMIT_MONTHLY_KEY)?.toInt()) ?: DEFAULT_EMAIL_LIMIT_MONTHLY - emailSizeLimit = (settings?.get(EMAIL_SIZE_LIMIT_KEY)?.toInt()) ?: DEFAULT_EMAIL_SIZE_LIMIT - sesAwsRegion = (settings?.get(EMAIL_SES_AWS_REGION_KEY) ?: DEFAULT_SES_AWS_REGION) - smtpHost = (settings?.get(EMAIL_SMTP_HOST_KEY) ?: DEFAULT_SMTP_HOST) - smtpPort = (settings?.get(EMAIL_SMTP_PORT_KEY)?.toInt()) ?: DEFAULT_SMTP_PORT - smtpTransportMethod = (settings?.get(EMAIL_SMTP_TRANSPORT_METHOD_KEY) ?: DEFAULT_SMTP_TRANSPORT_METHOD) adminAccess = AdminAccess.valueOf(settings?.get(ADMIN_ACCESS_KEY) ?: DEFAULT_ADMIN_ACCESS_METHOD) filterBy = FilterBy.valueOf(settings?.get(FILTER_BY_KEY) ?: DEFAULT_FILTER_BY_METHOD) ignoredRoles = settings?.getAsList(IGNORE_ROLE_KEY) ?: DEFAULT_IGNORED_ROLES @@ -346,14 +197,6 @@ internal object PluginSettings { defaultSettings = mapOf( OPERATION_TIMEOUT_MS_KEY to operationTimeoutMs.toString(DECIMAL_RADIX), DEFAULT_ITEMS_QUERY_COUNT_KEY to defaultItemsQueryCount.toString(DECIMAL_RADIX), - EMAIL_CHANNEL_KEY to emailChannel, - EMAIL_FROM_ADDRESS_KEY to emailFromAddress, - EMAIL_LIMIT_MONTHLY_KEY to emailMonthlyLimit.toString(DECIMAL_RADIX), - EMAIL_SIZE_LIMIT_KEY to emailSizeLimit.toString(DECIMAL_RADIX), - EMAIL_SES_AWS_REGION_KEY to sesAwsRegion, - EMAIL_SMTP_HOST_KEY to smtpHost, - EMAIL_SMTP_PORT_KEY to smtpPort.toString(DECIMAL_RADIX), - EMAIL_SMTP_TRANSPORT_METHOD_KEY to smtpTransportMethod, ADMIN_ACCESS_KEY to adminAccess.name, FILTER_BY_KEY to filterBy.name ) @@ -373,57 +216,6 @@ internal object PluginSettings { NodeScope, Dynamic ) - private val EMAIL_CHANNEL: Setting = Setting.simpleString( - EMAIL_CHANNEL_KEY, - defaultSettings[EMAIL_CHANNEL_KEY], - NodeScope, Dynamic - ) - - private val EMAIL_FROM_ADDRESS: Setting = Setting.simpleString( - EMAIL_FROM_ADDRESS_KEY, - defaultSettings[EMAIL_FROM_ADDRESS_KEY], - NodeScope, Dynamic - ) - - private val EMAIL_LIMIT_MONTHLY: Setting = Setting.intSetting( - EMAIL_LIMIT_MONTHLY_KEY, - defaultSettings[EMAIL_LIMIT_MONTHLY_KEY]!!.toInt(), - 0, - NodeScope, Dynamic - ) - - private val EMAIL_SIZE_LIMIT: Setting = Setting.intSetting( - EMAIL_SIZE_LIMIT_KEY, - defaultSettings[EMAIL_SIZE_LIMIT_KEY]!!.toInt(), - MINIMUM_EMAIL_SIZE_LIMIT, - NodeScope, Dynamic - ) - - private val EMAIL_SES_AWS_REGION: Setting = Setting.simpleString( - EMAIL_SES_AWS_REGION_KEY, - defaultSettings[EMAIL_SES_AWS_REGION_KEY], - NodeScope, Dynamic - ) - - private val EMAIL_SMTP_HOST: Setting = Setting.simpleString( - EMAIL_SMTP_HOST_KEY, - defaultSettings[EMAIL_SMTP_HOST_KEY], - NodeScope, Dynamic - ) - - private val EMAIL_SMTP_PORT: Setting = Setting.intSetting( - EMAIL_SMTP_PORT_KEY, - defaultSettings[EMAIL_SMTP_PORT_KEY]!!.toInt(), - 0, - NodeScope, Dynamic - ) - - private val EMAIL_SMTP_TRANSPORT_METHOD: Setting = Setting.simpleString( - EMAIL_SMTP_TRANSPORT_METHOD_KEY, - defaultSettings[EMAIL_SMTP_TRANSPORT_METHOD_KEY], - NodeScope, Dynamic - ) - private val ADMIN_ACCESS: Setting = Setting.simpleString( ADMIN_ACCESS_KEY, defaultSettings[ADMIN_ACCESS_KEY]!!, @@ -452,14 +244,6 @@ internal object PluginSettings { return listOf( OPERATION_TIMEOUT_MS, DEFAULT_ITEMS_QUERY_COUNT, - EMAIL_CHANNEL, - EMAIL_FROM_ADDRESS, - EMAIL_LIMIT_MONTHLY, - EMAIL_SIZE_LIMIT, - EMAIL_SES_AWS_REGION, - EMAIL_SMTP_HOST, - EMAIL_SMTP_PORT, - EMAIL_SMTP_TRANSPORT_METHOD, ADMIN_ACCESS, FILTER_BY, IGNORED_ROLES @@ -473,14 +257,6 @@ internal object PluginSettings { private fun updateSettingValuesFromLocal(clusterService: ClusterService) { operationTimeoutMs = OPERATION_TIMEOUT_MS.get(clusterService.settings) defaultItemsQueryCount = DEFAULT_ITEMS_QUERY_COUNT.get(clusterService.settings) - emailChannel = EMAIL_CHANNEL.get(clusterService.settings) - emailFromAddress = EMAIL_FROM_ADDRESS.get(clusterService.settings) - emailMonthlyLimit = EMAIL_LIMIT_MONTHLY.get(clusterService.settings) - emailSizeLimit = EMAIL_SIZE_LIMIT.get(clusterService.settings) - sesAwsRegion = EMAIL_SES_AWS_REGION.get(clusterService.settings) - smtpHost = EMAIL_SMTP_HOST.get(clusterService.settings) - smtpPort = EMAIL_SMTP_PORT.get(clusterService.settings) - smtpTransportMethod = EMAIL_SMTP_TRANSPORT_METHOD.get(clusterService.settings) adminAccess = AdminAccess.valueOf(ADMIN_ACCESS.get(clusterService.settings)) filterBy = FilterBy.valueOf(FILTER_BY.get(clusterService.settings)) ignoredRoles = IGNORED_ROLES.get(clusterService.settings) @@ -502,46 +278,6 @@ internal object PluginSettings { log.debug("$LOG_PREFIX:$DEFAULT_ITEMS_QUERY_COUNT_KEY -autoUpdatedTo-> $clusterDefaultItemsQueryCount") defaultItemsQueryCount = clusterDefaultItemsQueryCount } - val clusterEmailChannel = clusterService.clusterSettings.get(EMAIL_CHANNEL) - if (clusterEmailChannel != null) { - log.debug("$LOG_PREFIX:$EMAIL_CHANNEL_KEY -autoUpdatedTo-> $clusterEmailChannel") - emailChannel = clusterEmailChannel - } - val clusterEmailFromAddress = clusterService.clusterSettings.get(EMAIL_FROM_ADDRESS) - if (clusterEmailFromAddress != null) { - log.debug("$LOG_PREFIX:$EMAIL_FROM_ADDRESS_KEY -autoUpdatedTo-> $clusterEmailFromAddress") - emailFromAddress = clusterEmailFromAddress - } - val clusterEmailMonthlyLimit = clusterService.clusterSettings.get(EMAIL_LIMIT_MONTHLY) - if (clusterEmailMonthlyLimit != null) { - log.debug("$LOG_PREFIX:$EMAIL_LIMIT_MONTHLY_KEY -autoUpdatedTo-> $clusterEmailMonthlyLimit") - emailMonthlyLimit = clusterEmailMonthlyLimit - } - val clusterEmailSizeLimit = clusterService.clusterSettings.get(EMAIL_SIZE_LIMIT) - if (clusterEmailSizeLimit != null) { - log.debug("$LOG_PREFIX:$EMAIL_SIZE_LIMIT_KEY -autoUpdatedTo-> $clusterEmailSizeLimit") - emailSizeLimit = clusterEmailSizeLimit - } - val clusterSesAwsRegion = clusterService.clusterSettings.get(EMAIL_SES_AWS_REGION) - if (clusterSesAwsRegion != null) { - log.debug("$LOG_PREFIX:$EMAIL_SES_AWS_REGION_KEY -autoUpdatedTo-> $clusterSesAwsRegion") - sesAwsRegion = clusterSesAwsRegion - } - val clusterSmtpHost = clusterService.clusterSettings.get(EMAIL_SMTP_HOST) - if (clusterSmtpHost != null) { - log.debug("$LOG_PREFIX:$EMAIL_SMTP_HOST_KEY -autoUpdatedTo-> $clusterSmtpHost") - smtpHost = clusterSmtpHost - } - val clusterSmtpPort = clusterService.clusterSettings.get(EMAIL_SMTP_PORT) - if (clusterSmtpPort != null) { - log.debug("$LOG_PREFIX:$EMAIL_SMTP_PORT_KEY -autoUpdatedTo-> $clusterSmtpPort") - smtpPort = clusterSmtpPort - } - val clusterSmtpTransportMethod = clusterService.clusterSettings.get(EMAIL_SMTP_TRANSPORT_METHOD) - if (clusterSmtpTransportMethod != null) { - log.debug("$LOG_PREFIX:$EMAIL_SMTP_TRANSPORT_METHOD_KEY -autoUpdatedTo-> $clusterSmtpTransportMethod") - smtpTransportMethod = clusterSmtpTransportMethod - } val clusterAdminAccess = clusterService.clusterSettings.get(ADMIN_ACCESS) if (clusterAdminAccess != null) { log.debug("$LOG_PREFIX:$ADMIN_ACCESS_KEY -autoUpdatedTo-> $clusterAdminAccess") @@ -577,38 +313,6 @@ internal object PluginSettings { defaultItemsQueryCount = it log.info("$LOG_PREFIX:$DEFAULT_ITEMS_QUERY_COUNT_KEY -updatedTo-> $it") } - clusterService.clusterSettings.addSettingsUpdateConsumer(EMAIL_CHANNEL) { - emailChannel = it - log.info("$LOG_PREFIX:$EMAIL_CHANNEL_KEY -updatedTo-> $it") - } - clusterService.clusterSettings.addSettingsUpdateConsumer(EMAIL_FROM_ADDRESS) { - emailFromAddress = it - log.info("$LOG_PREFIX:$EMAIL_FROM_ADDRESS_KEY -updatedTo-> $it") - } - clusterService.clusterSettings.addSettingsUpdateConsumer(EMAIL_LIMIT_MONTHLY) { - emailMonthlyLimit = it - log.info("$LOG_PREFIX:$EMAIL_LIMIT_MONTHLY_KEY -updatedTo-> $it") - } - clusterService.clusterSettings.addSettingsUpdateConsumer(EMAIL_SIZE_LIMIT) { - emailSizeLimit = it - log.info("$LOG_PREFIX:$EMAIL_SIZE_LIMIT_KEY -updatedTo-> $it") - } - clusterService.clusterSettings.addSettingsUpdateConsumer(EMAIL_SES_AWS_REGION) { - sesAwsRegion = it - log.info("$LOG_PREFIX:$EMAIL_SES_AWS_REGION_KEY -updatedTo-> $it") - } - clusterService.clusterSettings.addSettingsUpdateConsumer(EMAIL_SMTP_HOST) { - smtpHost = it - log.info("$LOG_PREFIX:$EMAIL_SMTP_HOST_KEY -updatedTo-> $it") - } - clusterService.clusterSettings.addSettingsUpdateConsumer(EMAIL_SMTP_PORT) { - smtpPort = it - log.info("$LOG_PREFIX:$EMAIL_SMTP_PORT_KEY -updatedTo-> $it") - } - clusterService.clusterSettings.addSettingsUpdateConsumer(EMAIL_SMTP_TRANSPORT_METHOD) { - smtpTransportMethod = it - log.info("$LOG_PREFIX:$EMAIL_SMTP_TRANSPORT_METHOD_KEY -updatedTo-> $it") - } clusterService.clusterSettings.addSettingsUpdateConsumer(ADMIN_ACCESS) { adminAccess = AdminAccess.valueOf(it) log.info("$LOG_PREFIX:$ADMIN_ACCESS_KEY -updatedTo-> $it") diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/Accountant.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/Accountant.kt deleted file mode 100644 index 9118e3cf..00000000 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/Accountant.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.notifications.throttle - -import org.opensearch.client.Client -import org.opensearch.cluster.service.ClusterService -import org.opensearch.notifications.settings.PluginSettings -import java.util.Date - -/** - * The object class for keep track of the messages sent and provide throttle data. - */ -internal object Accountant { - private var messageCounter: MessageCounter = EmptyMessageCounter - - /** - * Initialize the class - * @param client The client - * @param clusterService The cluster service - */ - fun initialize(client: Client, clusterService: ClusterService) { - this.messageCounter = CounterIndex(client, clusterService) - } - - /** - * Increment the counters by provided value - * @param counters the counter object - */ - fun incrementCounters(counters: Counters) { - messageCounter.incrementCountersForDay(Date(), counters) - } - - /** - * Check if message quota is available - * @param counters the counter object - * @return true if message quota is available, false otherwise - */ - fun isMessageQuotaAvailable(counters: Counters): Boolean { - val monthlyCounters = messageCounter.getCounterForMonth(Date()) - monthlyCounters.incrementCountersBy(counters) - return monthlyCounters.emailSentSuccessCount.get() <= PluginSettings.emailMonthlyLimit - } -} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/CounterIndex.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/CounterIndex.kt deleted file mode 100644 index ed61a6c7..00000000 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/CounterIndex.kt +++ /dev/null @@ -1,253 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.notifications.throttle - -import org.opensearch.ResourceAlreadyExistsException -import org.opensearch.action.DocWriteResponse -import org.opensearch.action.admin.indices.create.CreateIndexRequest -import org.opensearch.action.get.GetRequest -import org.opensearch.action.index.IndexRequest -import org.opensearch.action.search.SearchRequest -import org.opensearch.action.update.UpdateRequest -import org.opensearch.client.Client -import org.opensearch.cluster.service.ClusterService -import org.opensearch.common.unit.TimeValue -import org.opensearch.common.xcontent.LoggingDeprecationHandler -import org.opensearch.common.xcontent.NamedXContentRegistry -import org.opensearch.common.xcontent.XContentType -import org.opensearch.commons.utils.logger -import org.opensearch.index.engine.DocumentMissingException -import org.opensearch.index.engine.VersionConflictEngineException -import org.opensearch.index.query.QueryBuilders -import org.opensearch.index.seqno.SequenceNumbers -import org.opensearch.notifications.NotificationPlugin.Companion.LOG_PREFIX -import org.opensearch.notifications.settings.PluginSettings -import org.opensearch.notifications.throttle.CounterIndexModel.Companion.COUNTER_INDEX_MODEL_KEY -import org.opensearch.notifications.throttle.CounterIndexModel.Companion.MAX_ITEMS_IN_MONTH -import org.opensearch.notifications.throttle.CounterIndexModel.Companion.getIdForDate -import org.opensearch.notifications.throttle.CounterIndexModel.Companion.getIdForStartOfMonth -import org.opensearch.notifications.util.SecureIndexClient -import org.opensearch.search.builder.SearchSourceBuilder -import java.util.Date -import java.util.concurrent.TimeUnit - -/** - * Class for doing index operation to maintain counters in cluster. - */ -internal class CounterIndex(client: Client, private val clusterService: ClusterService) : MessageCounter { - private val client: Client - - init { - this.client = SecureIndexClient(client) - } - - internal companion object { - private val log by logger(CounterIndex::class.java) - private const val COUNTER_INDEX_NAME = ".opensearch-notifications-counter" - private const val COUNTER_INDEX_SCHEMA_FILE_NAME = "opensearch-notifications-counter.yml" - private const val COUNTER_INDEX_SETTINGS_FILE_NAME = "opensearch-notifications-counter-settings.yml" - private const val MAPPING_TYPE = "_doc" - } - - /** - * {@inheritDoc} - */ - override fun getCounterForMonth(counterDay: Date): Counters { - val retValue = Counters() - if (!isIndexExists()) { - createIndex() - } else { - val startDay = getIdForStartOfMonth(counterDay) - val currentDay = getIdForDate(counterDay) - val query = QueryBuilders.rangeQuery(COUNTER_INDEX_MODEL_KEY).gte(startDay).lte(currentDay) - val sourceBuilder = SearchSourceBuilder() - .timeout(TimeValue(PluginSettings.operationTimeoutMs, TimeUnit.MILLISECONDS)) - .size(MAX_ITEMS_IN_MONTH) - .from(0) - .query(query) - val searchRequest = SearchRequest() - .indices(COUNTER_INDEX_NAME) - .source(sourceBuilder) - val actionFuture = client.search(searchRequest) - val response = actionFuture.actionGet(PluginSettings.operationTimeoutMs) - response.hits.forEach { - val parser = XContentType.JSON.xContent().createParser( - NamedXContentRegistry.EMPTY, - LoggingDeprecationHandler.INSTANCE, - it.sourceAsString - ) - parser.nextToken() - val modelValues = CounterIndexModel.parse(parser) - retValue.requestCount.addAndGet(modelValues.requestCount) - retValue.emailSentSuccessCount.addAndGet(modelValues.emailSentSuccessCount) - retValue.emailSentFailureCount.addAndGet(modelValues.emailSentFailureCount) - } - log.info("$LOG_PREFIX:getCounterForMonth:$retValue") - } - return retValue - } - - /** - * {@inheritDoc} - */ - @Suppress("TooGenericExceptionCaught") - override fun incrementCountersForDay(counterDay: Date, counters: Counters) { - if (!isIndexExists()) { - createIndex() - } - var isIncremented = false - while (!isIncremented) { - isIncremented = try { - incrementCounterIndexFor(counterDay, counters) - } catch (ignored: VersionConflictEngineException) { - log.info("$LOG_PREFIX:VersionConflictEngineException retrying") - false - } catch (ignored: DocumentMissingException) { - log.info("$LOG_PREFIX:DocumentMissingException retrying") - false - } - } - } - - /** - * Create index using the schema defined in resource - */ - @Suppress("TooGenericExceptionCaught") - private fun createIndex() { - val indexMappingSource = - CounterIndex::class.java.classLoader.getResource(COUNTER_INDEX_SCHEMA_FILE_NAME)?.readText()!! - val indexSettingsSource = - CounterIndex::class.java.classLoader.getResource(COUNTER_INDEX_SETTINGS_FILE_NAME)?.readText()!! - val request = CreateIndexRequest(COUNTER_INDEX_NAME) - .mapping(MAPPING_TYPE, indexMappingSource, XContentType.YAML) - .settings(indexSettingsSource, XContentType.YAML) - try { - val actionFuture = client.admin().indices().create(request) - val response = actionFuture.actionGet(PluginSettings.operationTimeoutMs) - if (response.isAcknowledged) { - log.info("$LOG_PREFIX:Index $COUNTER_INDEX_NAME creation Acknowledged") - } else { - throw IllegalStateException("$LOG_PREFIX:Index $COUNTER_INDEX_NAME creation not Acknowledged") - } - } catch (exception: Exception) { - if (exception !is ResourceAlreadyExistsException && exception.cause !is ResourceAlreadyExistsException) { - throw exception - } - } - } - - /** - * Check if the index is created and available. - * @return true if index is available, false otherwise - */ - private fun isIndexExists(): Boolean { - val clusterState = clusterService.state() - return clusterState.routingTable.hasIndex(COUNTER_INDEX_NAME) - } - - /** - * Query index for counter for given day - * @param counterDay the counter day - * @return counter index model - */ - private fun getCounterIndexFor(counterDay: Date): CounterIndexModel { - val getRequest = GetRequest(COUNTER_INDEX_NAME).id(getIdForDate(counterDay)) - val actionFuture = client.get(getRequest) - val response = actionFuture.actionGet(PluginSettings.operationTimeoutMs) - return if (response.sourceAsString == null) { - CounterIndexModel(counterDay, 0, 0, 0) - } else { - val parser = XContentType.JSON.xContent().createParser( - NamedXContentRegistry.EMPTY, - LoggingDeprecationHandler.INSTANCE, - response.sourceAsString - ) - parser.nextToken() - val retValue = CounterIndexModel.parse(parser, response.seqNo, response.primaryTerm) - if (getIdForDate(retValue.counterDay) == getIdForDate(counterDay)) { - CounterIndexModel(counterDay, 0, 0, 0) - } - retValue - } - } - - /** - * create a new doc for counter for given day - * @param counterDay the counter day - * @param counters the initial counter values - * @return true if successful, false otherwise - */ - private fun createCounterIndexFor(counterDay: Date, counters: Counters): Boolean { - val indexRequest = IndexRequest(COUNTER_INDEX_NAME) - .id(getIdForDate(counterDay)) - .source(CounterIndexModel.getCounterIndexModel(counterDay, counters).toXContent()) - .setIfSeqNo(SequenceNumbers.UNASSIGNED_SEQ_NO) - .setIfPrimaryTerm(SequenceNumbers.UNASSIGNED_PRIMARY_TERM) - .create(true) - val actionFuture = client.index(indexRequest) - val response = actionFuture.actionGet(PluginSettings.operationTimeoutMs) - log.debug("$LOG_PREFIX:CounterIndex createCounterIndex - $counters status:${response.result}") - return response.result == DocWriteResponse.Result.CREATED - } - - /** - * update existing doc for counter for given day - * @param counterDay the counter day - * @param counterIndexModel the counter index to update - * @return true if successful, false otherwise - */ - private fun updateCounterIndexFor(counterDay: Date, counterIndexModel: CounterIndexModel): Boolean { - val updateRequest = UpdateRequest() - .index(COUNTER_INDEX_NAME) - .id(getIdForDate(counterDay)) - .setIfSeqNo(counterIndexModel.seqNo) - .setIfPrimaryTerm(counterIndexModel.primaryTerm) - .doc(counterIndexModel.toXContent()) - .fetchSource(true) - val actionFuture = client.update(updateRequest) - val response = actionFuture.actionGet(PluginSettings.operationTimeoutMs) - log.debug("$LOG_PREFIX:CounterIndex updateCounterIndex - $counterIndexModel status:${response.result}") - return response.result == DocWriteResponse.Result.UPDATED - } - - /** - * create or update doc with counter added to existing value - * @param counterDay the counter day - * @param counters the counter values to increment - * @return true if successful, false otherwise - */ - private fun incrementCounterIndexFor(counterDay: Date, counters: Counters): Boolean { - val currentValue = getCounterIndexFor(counterDay) - log.debug("$LOG_PREFIX:CounterIndex currentValue - $currentValue") - return if (currentValue.seqNo == SequenceNumbers.UNASSIGNED_SEQ_NO) { - createCounterIndexFor(counterDay, counters) - } else { - updateCounterIndexFor(counterDay, currentValue.copyAndIncrementBy(counters)) - } - } -} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/CounterIndexModel.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/CounterIndexModel.kt deleted file mode 100644 index 01343308..00000000 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/CounterIndexModel.kt +++ /dev/null @@ -1,190 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.notifications.throttle - -import org.opensearch.common.xcontent.ToXContent -import org.opensearch.common.xcontent.ToXContentObject -import org.opensearch.common.xcontent.XContentBuilder -import org.opensearch.common.xcontent.XContentFactory -import org.opensearch.common.xcontent.XContentParser -import org.opensearch.common.xcontent.XContentParser.Token.END_OBJECT -import org.opensearch.common.xcontent.XContentParser.Token.START_OBJECT -import org.opensearch.common.xcontent.XContentParserUtils -import org.opensearch.commons.utils.logger -import org.opensearch.index.seqno.SequenceNumbers -import org.opensearch.notifications.NotificationPlugin.Companion.LOG_PREFIX -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Date -import java.util.Locale -import java.util.TimeZone - -/** - * Data class representing the doc for a day. - */ -internal data class CounterIndexModel( - val counterDay: Date, - val requestCount: Int, - val emailSentSuccessCount: Int, - val emailSentFailureCount: Int, - val seqNo: Long = SequenceNumbers.UNASSIGNED_SEQ_NO, - val primaryTerm: Long = SequenceNumbers.UNASSIGNED_PRIMARY_TERM -) : ToXContentObject { - internal companion object { - private val log by logger(CounterIndexModel::class.java) - private const val COUNTER_DAY_TAG = "counter_day" - private const val REQUEST_COUNT_TAG = "request_count" - private const val EMAIL_SENT_SUCCESS_COUNT = "email_sent_success_count" - private const val EMAIL_SENT_FAILURE_COUNT = "email_sent_failure_count" - private val DATE_FORMATTER = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT) - - const val COUNTER_INDEX_MODEL_KEY = COUNTER_DAY_TAG - const val MAX_ITEMS_IN_MONTH = 31 - - /** - * get the ID for a given date - * @param day the day to create ID - * @return ID for the day - */ - fun getIdForDate(day: Date): String { - return DATE_FORMATTER.format(day) - } - - /** - * get the ID for beginning of the month of a given Instant - * @param day the reference day to create ID - * @return ID for the beginning of the month - */ - fun getIdForStartOfMonth(day: Date): String { - return getIdForDate(getFirstDateOfMonth(day)) - } - - private fun getFirstDateOfMonth(date: Date): Date { - val cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"), Locale.ROOT) - cal.time = date - cal[Calendar.DAY_OF_MONTH] = cal.getActualMinimum(Calendar.DAY_OF_MONTH) - return cal.time - } - - /** - * get/create Counter index model from counters - * @param day the day to create model - * @param counters the counter values - * @return created counter index model - */ - fun getCounterIndexModel(day: Date, counters: Counters): CounterIndexModel { - return CounterIndexModel( - day, - counters.requestCount.get(), - counters.emailSentSuccessCount.get(), - counters.emailSentFailureCount.get() - ) - } - - /** - * Parse the data from parser and create Counter index model - * @param parser data referenced at parser - * @param seqNo the seqNo of the document - * @param primaryTerm the primaryTerm of the document - * @return created counter index model - */ - fun parse( - parser: XContentParser, - seqNo: Long = SequenceNumbers.UNASSIGNED_SEQ_NO, - primaryTerm: Long = SequenceNumbers.UNASSIGNED_PRIMARY_TERM - ): CounterIndexModel { - var counterDay: Date? = null - var requestCount: Int? = null - var emailSentSuccessCount: Int? = null - var emailSentFailureCount: Int? = null - XContentParserUtils.ensureExpectedToken(START_OBJECT, parser.currentToken(), parser) - while (END_OBJECT != parser.nextToken()) { - val fieldName = parser.currentName() - parser.nextToken() - when (fieldName) { - COUNTER_DAY_TAG -> counterDay = DATE_FORMATTER.parse(parser.text()) - REQUEST_COUNT_TAG -> requestCount = parser.intValue() - EMAIL_SENT_SUCCESS_COUNT -> emailSentSuccessCount = parser.intValue() - EMAIL_SENT_FAILURE_COUNT -> emailSentFailureCount = parser.intValue() - else -> { - parser.skipChildren() - log.warn("$LOG_PREFIX:Skipping Unknown field $fieldName") - } - } - } - counterDay ?: throw IllegalArgumentException("$COUNTER_DAY_TAG field not present") - requestCount ?: throw IllegalArgumentException("$REQUEST_COUNT_TAG field not present") - emailSentSuccessCount ?: throw IllegalArgumentException("$EMAIL_SENT_SUCCESS_COUNT field not present") - emailSentFailureCount ?: throw IllegalArgumentException("$EMAIL_SENT_FAILURE_COUNT field not present") - return CounterIndexModel( - counterDay, - requestCount, - emailSentSuccessCount, - emailSentFailureCount, - seqNo, - primaryTerm - ) - } - } - - /** - * copy/create Counter index model from this object - * @param counters the counter values to add to this object - * @return created counter index model - */ - fun copyAndIncrementBy(counters: Counters): CounterIndexModel { - return copy( - requestCount = requestCount + counters.requestCount.get(), - emailSentSuccessCount = emailSentSuccessCount + counters.emailSentSuccessCount.get(), - emailSentFailureCount = emailSentFailureCount + counters.emailSentFailureCount.get() - ) - } - - /** - * create XContentBuilder from this object using [XContentFactory.jsonBuilder()] - * @return created XContentBuilder object - */ - fun toXContent(): XContentBuilder? { - return toXContent(XContentFactory.jsonBuilder(), ToXContent.EMPTY_PARAMS) - } - - /** - * {@inheritDoc} - */ - override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder? { - if (builder != null) { - builder.startObject() - .field(COUNTER_DAY_TAG, getIdForDate(counterDay)) - .field(REQUEST_COUNT_TAG, requestCount) - .field(EMAIL_SENT_SUCCESS_COUNT, emailSentSuccessCount) - .field(EMAIL_SENT_FAILURE_COUNT, emailSentFailureCount) - .endObject() - } - return builder - } -} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/Counters.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/Counters.kt deleted file mode 100644 index d656dc12..00000000 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/Counters.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.notifications.throttle - -import java.util.concurrent.atomic.AtomicInteger - -/** - * Counter class to maintain the counting of the items - */ -internal class Counters { - /** - * Number of requests. - */ - val requestCount = AtomicInteger() - - /** - * Number of email sent successfully - */ - val emailSentSuccessCount = AtomicInteger() - - /** - * Number of email request failed - */ - val emailSentFailureCount = AtomicInteger() - - /** - * Increment the counters by given counter values - * @param counters The counter values to increment - */ - fun incrementCountersBy(counters: Counters) { - requestCount.addAndGet(counters.requestCount.get()) - emailSentSuccessCount.addAndGet(counters.emailSentSuccessCount.get()) - emailSentFailureCount.addAndGet(counters.emailSentFailureCount.get()) - } - - /** - * {@inheritDoc} - */ - override fun toString(): String { - return "{requestCount=$requestCount, emailSentSuccessCount=$emailSentSuccessCount, emailSentFailureCount=$emailSentFailureCount}" - } -} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/EmptyMessageCounter.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/EmptyMessageCounter.kt deleted file mode 100644 index 9802c71d..00000000 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/EmptyMessageCounter.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.notifications.throttle - -import java.util.Date - -/** - * Empty implementation of the message counter which responds with IllegalStateException all operations. - */ -internal object EmptyMessageCounter : MessageCounter { - /** - * {@inheritDoc} - */ - override fun incrementCountersForDay(counterDay: Date, counters: Counters) { - throw IllegalStateException("MessageCounter not initialized") - } - - /** - * {@inheritDoc} - */ - override fun getCounterForMonth(counterDay: Date): Counters { - throw IllegalStateException("MessageCounter not initialized") - } -} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/MessageCounter.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/MessageCounter.kt deleted file mode 100644 index a5764e05..00000000 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/throttle/MessageCounter.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.notifications.throttle - -import java.util.Date - -/** - * Message counter interface - */ -internal interface MessageCounter { - /** - * Increment the each values in the counters for the day in the index. - * @param counterDay the reference day - * @param counters the counter values to increment - */ - fun incrementCountersForDay(counterDay: Date, counters: Counters) - - /** - * Get the current counters for the month from the index. - * @param counterDay the reference day - * @return the counters with values corresponds to month of counterDay. - */ - fun getCounterForMonth(counterDay: Date): Counters -} diff --git a/notifications/notifications/src/main/resources/notifications-config-mapping.yml b/notifications/notifications/src/main/resources/notifications-config-mapping.yml index 8f1086b5..d2ff05a2 100644 --- a/notifications/notifications/src/main/resources/notifications-config-mapping.yml +++ b/notifications/notifications/src/main/resources/notifications-config-mapping.yml @@ -104,9 +104,15 @@ properties: type: object properties: topic_arn: - type: keyword + type: text + fields: + keyword: + type: keyword role_arn: - type: keyword + type: text + fields: + keyword: + type: keyword smtp_account: # smtp account configuration type: object properties: diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/NotificationsRestTestCase.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/NotificationsRestTestCase.kt deleted file mode 100644 index 54caac55..00000000 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/NotificationsRestTestCase.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.integtest - -import com.google.gson.JsonObject -import org.junit.After -import org.junit.Before -import org.opensearch.client.Request -import org.opensearch.client.RequestOptions -import org.opensearch.notifications.resthandler.SendMessageRestHandler.Companion.SEND_BASE_URI -import org.opensearch.notifications.settings.PluginSettings -import org.springframework.integration.test.mail.TestMailServer - -abstract class NotificationsRestTestCase : PluginRestTestCase() { - - private val smtpPort = PluginSettings.smtpPort - private val smtpServer: TestMailServer.SmtpServer - private val fromAddress = "from@email.com" - - init { - smtpServer = TestMailServer.smtp(smtpPort) - } - - @Before - @Throws(InterruptedException::class) - fun setupNotification() { - resetFromAddress() - init() - } - - @After - open fun tearDownServer() { - smtpServer.stop() - smtpServer.resetServer() - } - - protected fun executeRequest( - refTag: String, - recipients: List, - title: String, - textDescription: String, - htmlDescription: String, - attachment: JsonObject - ): JsonObject { - val request = buildRequest(refTag, recipients, title, textDescription, htmlDescription, attachment) - return executeRequest(request) - } - - protected fun buildRequest( - refTag: String, - recipients: List, - title: String, - textDescription: String, - htmlDescription: String, - attachment: JsonObject - ): Request { - val request = Request("POST", SEND_BASE_URI) - - val jsonEntity = NotificationsJsonEntity.Builder() - .setRefTag(refTag) - .setRecipients(recipients) - .setTitle(title) - .setTextDescription(textDescription) - .setHtmlDescription(htmlDescription) - .setAttachment(attachment.toString()) - .build() - request.setJsonEntity(jsonEntity.getJsonEntityAsString()) - - val restOptionsBuilder = RequestOptions.DEFAULT.toBuilder() - restOptionsBuilder.addHeader("Content-Type", "application/json") - request.setOptions(restOptionsBuilder) - return request - } - - /** Provided for each test to load test index, data and other setup work */ - protected open fun init() {} - - protected fun setFromAddress(address: String): JsonObject? { - return updateClusterSettings( - ClusterSetting( - "persistent", "opensearch.notifications.email.fromAddress", address - ) - ) - } - - protected fun resetFromAddress(): JsonObject? { - return setFromAddress(fromAddress) - } - - protected fun setChannelType(type: String) { - updateClusterSettings( - ClusterSetting( - "persistent", "opensearch.notifications.email.channel", type - ) - ) - } - - protected fun resetChannelType() { - setChannelType(PluginSettings.emailChannel) - } -} diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/channel/SesChannelIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/channel/SesChannelIT.kt deleted file mode 100644 index 02a8b73c..00000000 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/channel/SesChannelIT.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.integtest.channel - -import org.junit.After -import org.opensearch.integtest.NotificationsRestTestCase -import org.opensearch.integtest.getStatusCode -import org.opensearch.integtest.getStatusText -import org.opensearch.integtest.jsonify -import org.opensearch.rest.RestStatus - -class SesChannelIT : NotificationsRestTestCase() { - private val refTag = "ref" - private val title = "title" - private val textDescription = "text" - private val htmlDescription = "html" - private val attachment = jsonify( - """ - { - "file_name": "odfe.data", - "file_encoding": "base64", - "file_content_type": "application/octet-stream", - "file_data": "VGVzdCBtZXNzYWdlCgo=" - } - """.trimIndent() - ) - - override fun init() { - setChannelType("ses") - } - - @After - fun reset() { - resetChannelType() - } - - fun `test send email over ses channel due to ses authorization failure`() { - val recipients = listOf("mailto:test@localhost") - val response = executeRequest(refTag, recipients, title, textDescription, htmlDescription, attachment) - - val statusCode = getStatusCode(response) - assertEquals(RestStatus.FAILED_DEPENDENCY.status, statusCode) - - val statusText = getStatusText(response) - assertEquals("sendEmail Error, SES status:403:Optional[Forbidden]", statusText) - } -} diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/channel/SmtpChannelIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/channel/SmtpChannelIT.kt deleted file mode 100644 index 5f0878af..00000000 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/channel/SmtpChannelIT.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.integtest.channel - -import org.opensearch.integtest.NotificationsRestTestCase -import org.opensearch.integtest.getStatusCode -import org.opensearch.integtest.getStatusText -import org.opensearch.integtest.jsonify -import org.opensearch.integtest.verifyResponse -import org.opensearch.notifications.settings.PluginSettings -import org.opensearch.rest.RestStatus - -internal class SmtpChannelIT : NotificationsRestTestCase() { - private val refTag = "sample ref name" - private val title = "sample title" - private val textDescription = "Description for notification in text" - private val htmlDescription = "Description for notification in json encode html format" - private val attachment = jsonify( - """ - { - "file_name": "odfe.data", - "file_encoding": "base64", - "file_content_type": "application/octet-stream", - "file_data": "VGVzdCBtZXNzYWdlCgo=" - } - """.trimIndent() - ) - - fun `test send email to one recipient over Smtp server`() { - val recipients = listOf("mailto:test@localhost") - val response = executeRequest(refTag, recipients, title, textDescription, htmlDescription, attachment) - verifyResponse(response, refTag, recipients) - } - - fun `test send email to multiple recipient over Smtp server`() { - val recipients = listOf("mailto:test1@localhost", "mailto:test2@abc.com", "mailto:test3@123.com") - val response = executeRequest(refTag, recipients, title, textDescription, htmlDescription, attachment) - verifyResponse(response, refTag, recipients) - } - - fun `test send email with unconfigured address`() { - setFromAddress(PluginSettings.UNCONFIGURED_EMAIL_ADDRESS) - val recipients = listOf("mailto:test@localhost") - val response = executeRequest(refTag, recipients, title, textDescription, htmlDescription, attachment) - - val statusCode = getStatusCode(response) - assertEquals(RestStatus.NOT_IMPLEMENTED.status, statusCode) - - val statusText = getStatusText(response) - assertEquals("Email from: address not configured", statusText) - resetFromAddress() - } -} diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/resthandler/SendMessageRestHandlerTests.kt b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/resthandler/SendMessageRestHandlerTests.kt deleted file mode 100644 index af94d50e..00000000 --- a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/resthandler/SendMessageRestHandlerTests.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Copyright 2020 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 org.opensearch.notifications.resthandler - -import org.junit.jupiter.api.Test -import org.opensearch.rest.RestHandler -import org.opensearch.rest.RestRequest.Method.POST -import org.opensearch.test.OpenSearchTestCase - -internal class SendMessageRestHandlerTests : OpenSearchTestCase() { - - @Test - fun `SendRestHandler name should return send`() { - val restHandler = SendMessageRestHandler() - assertEquals("send_message", restHandler.name) - } - - @Test - fun `SendRestHandler routes should return send url`() { - val restHandler = SendMessageRestHandler() - val routes = restHandler.routes() - val actualRouteSize = routes.size - val actualRoute = routes[0] - val expectedRoute = RestHandler.Route(POST, "/_plugins/_notifications/send") - assertEquals(1, actualRouteSize) - assertEquals(expectedRoute.method, actualRoute.method) - assertEquals(expectedRoute.path, actualRoute.path) - } -} diff --git a/notifications/spi/build.gradle b/notifications/spi/build.gradle index 48c0a652..9585d858 100644 --- a/notifications/spi/build.gradle +++ b/notifications/spi/build.gradle @@ -93,16 +93,22 @@ configurations.all { force "commons-codec:commons-codec:1.13" // resolve for awssdk:ses force "io.netty:netty-codec-http:4.1.63.Final" // resolve for awssdk:ses force "io.netty:netty-handler:4.1.63.Final" // resolve for awssdk:ses + force "io.netty:netty-common:4.1.63.Final" // resolve for awssdk:ses + force "io.netty:netty-buffer:4.1.63.Final" // resolve for awssdk:ses + force "io.netty:netty-transport:4.1.63.Final" // resolve for awssdk:ses + force "io.netty:netty-codec:4.1.63.Final" // resolve for awssdk:ses force "org.apache.httpcomponents:httpclient:4.5.10" // resolve for awssdk:ses force "org.apache.httpcomponents:httpcore:4.4.13" // resolve for awssdk:ses - // Resolve for awssdk:sns force "joda-time:joda-time:2.8.1" - force "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.12.3" - force "com.fasterxml.jackson.core:jackson-core:2.12.3" - force "com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.12.3" - force "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.12.3" - force "junit:junit:4.12" + force "com.fasterxml.jackson.core:jackson-core:2.12.3" // resolve for awssdk:ses + force "com.fasterxml.jackson.core:jackson-annotations:2.12.3" // resolve for awssdk:ses + force "com.fasterxml.jackson.core:jackson-databind:2.12.3" // resolve for awssdk:ses + force "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.12.3" // resolve for awssdk:ses + force "com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.12.3" // resolve for awssdk:ses + force "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.12.3" // resolve for awssdk:ses + force "org.reactivestreams:reactive-streams:1.0.3" // resolve for awssdk:ses + force "junit:junit:4.12" // resolve for awssdk:ses } } @@ -114,10 +120,9 @@ dependencies { compile "org.apache.httpcomponents:httpclient:4.5.10" compile "com.amazonaws:aws-java-sdk-sns:${aws_version}" compile "com.amazonaws:aws-java-sdk-sts:${aws_version}" - //TODO: Add it back and remove from main project(to avoid jarhell) when implementing Email functionality -// compile ("software.amazon.awssdk:ses:2.14.16") { -// exclude module: 'annotations' // conflict with org.jetbrains:annotations, integTestRunner fails with error "codebase property already set" -// } + compile ("software.amazon.awssdk:ses:2.14.16") { + exclude module: 'annotations' // conflict with org.jetbrains:annotations, integTestRunner fails with error "codebase property already set" + } compile "com.sun.mail:javax.mail:1.6.2" implementation "com.github.seancfoley:ipaddress:5.3.3" testImplementation( diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/NotificationSpi.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/NotificationSpi.kt index 4b9513db..9e57c863 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/NotificationSpi.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/NotificationSpi.kt @@ -43,14 +43,20 @@ object NotificationSpi { /** * Send the notification message to the corresponding destination * + * @param destination destination configuration for sending message * @param message metadata for message + * @param referenceId referenceId for message * @return ChannelMessageResponse */ - fun sendMessage(destination: BaseDestination, message: MessageContent): DestinationMessageResponse { + fun sendMessage( + destination: BaseDestination, + message: MessageContent, + referenceId: String + ): DestinationMessageResponse { return AccessController.doPrivileged( PrivilegedAction { val destinationFactory = DestinationTransportProvider.getTransport(destination.destinationType) - destinationFactory.sendMessage(destination, message) + destinationFactory.sendMessage(destination, message, referenceId) } as PrivilegedAction? ) } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationClientPool.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationClientPool.kt index ecd622f8..dfad122b 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationClientPool.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationClientPool.kt @@ -27,7 +27,8 @@ package org.opensearch.notifications.spi.client -import org.opensearch.notifications.spi.model.destination.SNSDestination +import org.opensearch.notifications.spi.credentials.oss.SesClientFactoryImpl +import org.opensearch.notifications.spi.credentials.oss.SnsClientFactoryImpl /** * This class provides Client to the relevant destinations @@ -35,9 +36,6 @@ import org.opensearch.notifications.spi.model.destination.SNSDestination internal object DestinationClientPool { val httpClient: DestinationHttpClient = DestinationHttpClient() val smtpClient: DestinationSmtpClient = DestinationSmtpClient() - - // TODO: cache by cred and region? - fun getSNSClient(destination: SNSDestination): DestinationSNSClient { - return DestinationSNSClient(destination) - } + val snsClient: DestinationSnsClient = DestinationSnsClient(SnsClientFactoryImpl) + val sesClient: DestinationSesClient = DestinationSesClient(SesClientFactoryImpl) } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationHttpClient.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationHttpClient.kt index 76fdc4e6..3b7c1e92 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationHttpClient.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationHttpClient.kt @@ -110,12 +110,14 @@ class DestinationHttpClient { } @Throws(Exception::class) - fun execute(destination: WebhookDestination, message: MessageContent): String { + fun execute(destination: WebhookDestination, message: MessageContent, referenceId: String): String { var response: CloseableHttpResponse? = null return try { response = getHttpResponse(destination, message) validateResponseStatus(response) - getResponseString(response) + val responseString = getResponseString(response) + log.debug("Http response for id $referenceId: $responseString") + responseString } finally { if (response != null) { EntityUtils.consumeQuietly(response.entity) @@ -157,9 +159,7 @@ class DestinationHttpClient { @Throws(IOException::class) fun getResponseString(response: CloseableHttpResponse): String { val entity: HttpEntity = response.entity ?: return "{}" - val responseString: String = EntityUtils.toString(entity) - log.debug("Http response: $responseString") - return responseString + return EntityUtils.toString(entity) } @Throws(IOException::class) @@ -180,7 +180,7 @@ class DestinationHttpClient { is ChimeDestination -> "Content" is CustomWebhookDestination -> return message.textDescription else -> throw IllegalArgumentException( - "Invalid destination type is provided, Only Slack, Chime and CustomWebook are allowed" + "Invalid destination type is provided, Only Slack, Chime and CustomWebhook are allowed" ) } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSesClient.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSesClient.kt new file mode 100644 index 00000000..ffff5a49 --- /dev/null +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSesClient.kt @@ -0,0 +1,153 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.spi.client + +import org.opensearch.notifications.spi.NotificationSpiPlugin.Companion.LOG_PREFIX +import org.opensearch.notifications.spi.credentials.SesClientFactory +import org.opensearch.notifications.spi.model.DestinationMessageResponse +import org.opensearch.notifications.spi.model.MessageContent +import org.opensearch.notifications.spi.model.destination.SesDestination +import org.opensearch.notifications.spi.setting.PluginSettings +import org.opensearch.notifications.spi.utils.SecurityAccess +import org.opensearch.notifications.spi.utils.logger +import org.opensearch.rest.RestStatus +import software.amazon.awssdk.core.SdkBytes +import software.amazon.awssdk.core.exception.SdkException +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.ses.model.AccountSendingPausedException +import software.amazon.awssdk.services.ses.model.ConfigurationSetDoesNotExistException +import software.amazon.awssdk.services.ses.model.ConfigurationSetSendingPausedException +import software.amazon.awssdk.services.ses.model.MailFromDomainNotVerifiedException +import software.amazon.awssdk.services.ses.model.MessageRejectedException +import software.amazon.awssdk.services.ses.model.RawMessage +import software.amazon.awssdk.services.ses.model.SendRawEmailRequest +import software.amazon.awssdk.services.ses.model.SesException +import java.io.ByteArrayOutputStream +import java.util.Properties +import javax.mail.Session +import javax.mail.internet.MimeMessage + +/** + * This class handles the connections to the given Destination. + */ +class DestinationSesClient(private val sesClientFactory: SesClientFactory) { + + companion object { + private val log by logger(DestinationSesClient::class.java) + } + + /** + * {@inheritDoc} + */ + private fun prepareSession(): Session { + val prop = Properties() + prop["mail.transport.protocol"] = "smtp" + return Session.getInstance(prop) + } + + @Throws(Exception::class) + fun execute( + sesDestination: SesDestination, + message: MessageContent, + referenceId: String + ): DestinationMessageResponse { + if (EmailMessageValidator.isMessageSizeOverLimit(message)) { + return DestinationMessageResponse( + RestStatus.REQUEST_ENTITY_TOO_LARGE.status, + "Email size larger than ${PluginSettings.emailSizeLimit}" + ) + } + + // prepare session + val session = prepareSession() + // prepare mimeMessage + val mimeMessage = EmailMimeProvider.prepareMimeMessage( + session, + sesDestination.fromAddress, + sesDestination.recipient, + message + ) + // send Mime Message + return sendMimeMessage(referenceId, sesDestination.awsRegion, sesDestination.roleArn, mimeMessage) + } + + /** + * {@inheritDoc} + */ + private fun sendMimeMessage( + referenceId: String, + sesAwsRegion: String, + roleArn: String?, + mimeMessage: MimeMessage + ): DestinationMessageResponse { + return try { + log.debug("$LOG_PREFIX:Sending Email-SES:$referenceId") + val region = Region.of(sesAwsRegion) + val client = sesClientFactory.createSesClient(region, roleArn) + val outputStream = ByteArrayOutputStream() + SecurityAccess.doPrivileged { mimeMessage.writeTo(outputStream) } + val emailSize = outputStream.size() + if (emailSize <= PluginSettings.emailSizeLimit) { + val data = SdkBytes.fromByteArray(outputStream.toByteArray()) + val rawMessage = RawMessage.builder() + .data(data) + .build() + val rawEmailRequest = SendRawEmailRequest.builder() + .rawMessage(rawMessage) + .build() + val response = SecurityAccess.doPrivileged { client.sendRawEmail(rawEmailRequest) } + log.info("$LOG_PREFIX:Email-SES:$referenceId status:$response") + DestinationMessageResponse(RestStatus.OK.status, "Success") + } else { + DestinationMessageResponse( + RestStatus.REQUEST_ENTITY_TOO_LARGE.status, + "Email size($emailSize) larger than ${PluginSettings.emailSizeLimit}" + ) + } + } catch (exception: MessageRejectedException) { + DestinationMessageResponse(RestStatus.SERVICE_UNAVAILABLE.status, getSesExceptionText(exception)) + } catch (exception: MailFromDomainNotVerifiedException) { + DestinationMessageResponse(RestStatus.FORBIDDEN.status, getSesExceptionText(exception)) + } catch (exception: ConfigurationSetDoesNotExistException) { + DestinationMessageResponse(RestStatus.NOT_IMPLEMENTED.status, getSesExceptionText(exception)) + } catch (exception: ConfigurationSetSendingPausedException) { + DestinationMessageResponse(RestStatus.SERVICE_UNAVAILABLE.status, getSesExceptionText(exception)) + } catch (exception: AccountSendingPausedException) { + DestinationMessageResponse(RestStatus.INSUFFICIENT_STORAGE.status, getSesExceptionText(exception)) + } catch (exception: SesException) { + DestinationMessageResponse(RestStatus.FAILED_DEPENDENCY.status, getSesExceptionText(exception)) + } catch (exception: SdkException) { + DestinationMessageResponse(RestStatus.FAILED_DEPENDENCY.status, getSdkExceptionText(exception)) + } + } + + /** + * Create error string from Amazon SES Exceptions + * @param exception SES Exception + * @return generated error string + */ + private fun getSesExceptionText(exception: SesException): String { + val httpResponse = exception.awsErrorDetails().sdkHttpResponse() + log.info("$LOG_PREFIX:SesException $exception") + return "sendEmail Error, SES status:${httpResponse.statusCode()}:${httpResponse.statusText()}" + } + + /** + * Create error string from Amazon SDK Exceptions + * @param exception SDK Exception + * @return generated error string + */ + private fun getSdkExceptionText(exception: SdkException): String { + log.info("$LOG_PREFIX:SdkException $exception") + return "sendEmail Error, SDK status:${exception.message}" + } +} diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSmtpClient.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSmtpClient.kt index ca41d839..490146f8 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSmtpClient.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSmtpClient.kt @@ -41,8 +41,12 @@ class DestinationSmtpClient { } @Throws(Exception::class) - fun execute(smtpDestination: SmtpDestination, message: MessageContent): DestinationMessageResponse { - if (isMessageSizeOverLimit(message)) { + fun execute( + smtpDestination: SmtpDestination, + message: MessageContent, + referenceId: String + ): DestinationMessageResponse { + if (EmailMessageValidator.isMessageSizeOverLimit(message)) { return DestinationMessageResponse( RestStatus.REQUEST_ENTITY_TOO_LARGE.status, "Email size larger than ${PluginSettings.emailSizeLimit}" @@ -58,7 +62,8 @@ class DestinationSmtpClient { when (smtpDestination.method) { "ssl" -> prop["mail.smtp.ssl.enable"] = true "start_tls" -> prop["mail.smtp.starttls.enable"] = true - "none" -> {} + "none" -> { + } else -> throw IllegalArgumentException("Invalid method supplied") } @@ -81,15 +86,22 @@ class DestinationSmtpClient { } // prepare mimeMessage - val mimeMessage = EmailMimeProvider.prepareMimeMessage(session, smtpDestination, message) + val mimeMessage = EmailMimeProvider.prepareMimeMessage( + session, + smtpDestination.fromAddress, + smtpDestination.recipient, + message + ) // send Mime Message - return sendMimeMessage(mimeMessage) + return sendMimeMessage(mimeMessage, referenceId) } fun getSecureDestinationSetting(SmtpDestination: SmtpDestination): SecureDestinationSettings? { - val emailUsername: SecureString? = PluginSettings.destinationSettings[SmtpDestination.accountName]?.emailUsername - val emailPassword: SecureString? = PluginSettings.destinationSettings[SmtpDestination.accountName]?.emailPassword + val emailUsername: SecureString? = + PluginSettings.destinationSettings[SmtpDestination.accountName]?.emailUsername + val emailPassword: SecureString? = + PluginSettings.destinationSettings[SmtpDestination.accountName]?.emailPassword return if (emailUsername == null || emailPassword == null) { null } else { @@ -100,11 +112,11 @@ class DestinationSmtpClient { /** * {@inheritDoc} */ - private fun sendMimeMessage(mimeMessage: MimeMessage): DestinationMessageResponse { + private fun sendMimeMessage(mimeMessage: MimeMessage, referenceId: String): DestinationMessageResponse { return try { - log.debug("Sending Email-SMTP") + log.debug("Sending Email-SMTP for $referenceId") SecurityAccess.doPrivileged { sendMessage(mimeMessage) } - log.info("Email-SMTP sent") + log.info("Email-SMTP sent for $referenceId") DestinationMessageResponse(RestStatus.OK.status, "Success") } catch (exception: SendFailedException) { DestinationMessageResponse(RestStatus.BAD_GATEWAY.status, getMessagingExceptionText(exception)) @@ -132,20 +144,4 @@ class DestinationSmtpClient { log.info("EmailException $exception") return "sendEmail Error, status:${exception.message}" } - - private fun isMessageSizeOverLimit(message: MessageContent): Boolean { - val approxAttachmentLength = if (message.fileData != null && message.fileName != null) { - PluginSettings.emailMinimumHeaderLength + message.fileData.length + message.fileName.length - } else { - 0 - } - - val approxEmailLength = PluginSettings.emailMinimumHeaderLength + - message.title.length + - message.textDescription.length + - (message.htmlDescription?.length ?: 0) + - approxAttachmentLength - - return approxEmailLength > PluginSettings.emailSizeLimit - } } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSNSClient.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSnsClient.kt similarity index 52% rename from notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSNSClient.kt rename to notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSnsClient.kt index e9a88784..65ac3bcd 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSNSClient.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSnsClient.kt @@ -12,19 +12,18 @@ package org.opensearch.notifications.spi.client import com.amazonaws.services.sns.AmazonSNS -import org.opensearch.notifications.spi.credentials.oss.SNSClientFactory +import org.opensearch.notifications.spi.credentials.SnsClientFactory import org.opensearch.notifications.spi.model.MessageContent -import org.opensearch.notifications.spi.model.destination.SNSDestination +import org.opensearch.notifications.spi.model.destination.SnsDestination /** * This class handles the SNS connections to the given Destination. */ -class DestinationSNSClient(destination: SNSDestination) { +class DestinationSnsClient(private val snsClientFactory: SnsClientFactory) { - private val amazonSNS: AmazonSNS = SNSClientFactory().getClient(destination) - - fun execute(topicArn: String, message: MessageContent): String { - val result = amazonSNS.publish(topicArn, message.textDescription, message.title) + fun execute(destination: SnsDestination, message: MessageContent, referenceId: String): String { + val amazonSNS: AmazonSNS = snsClientFactory.createSnsClient(destination.region, destination.roleArn) + val result = amazonSNS.publish(destination.topicArn, message.textDescription, message.title) return result.messageId } } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/EmailMessageValidator.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/EmailMessageValidator.kt new file mode 100644 index 00000000..3b35a899 --- /dev/null +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/EmailMessageValidator.kt @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.spi.client + +import org.opensearch.notifications.spi.model.MessageContent +import org.opensearch.notifications.spi.setting.PluginSettings +import org.opensearch.notifications.spi.utils.logger + +/** + * This class handles the connections to the given Destination. + */ +internal object EmailMessageValidator { + private val log by logger(EmailMessageValidator::class.java) + fun isMessageSizeOverLimit(message: MessageContent): Boolean { + val approxAttachmentLength = if (message.fileData != null && message.fileName != null) { + PluginSettings.emailMinimumHeaderLength + message.fileData.length + message.fileName.length + } else { + 0 + } + + val approxEmailLength = PluginSettings.emailMinimumHeaderLength + + message.title.length + + message.textDescription.length + + (message.htmlDescription?.length ?: 0) + + approxAttachmentLength + + return approxEmailLength > PluginSettings.emailSizeLimit + } +} diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/EmailMimeProvider.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/EmailMimeProvider.kt index a2c5bea4..8e445f16 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/EmailMimeProvider.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/EmailMimeProvider.kt @@ -28,7 +28,6 @@ package org.opensearch.notifications.spi.client import org.opensearch.notifications.spi.model.MessageContent -import org.opensearch.notifications.spi.model.destination.SmtpDestination import java.util.Base64 import javax.activation.DataHandler import javax.mail.Message @@ -45,23 +44,25 @@ internal object EmailMimeProvider { /** * Create and prepare mime mimeMessage to send mail * @param session The mail session to use to create mime mimeMessage - * @param smtpDestination - * @param mimeMessage The mimeMessage to send notification + * @param fromAddress The sender email address + * @param recipient The recipient email address + * @param messageContent The mimeMessage to send notification * @return The created and prepared mime mimeMessage object */ fun prepareMimeMessage( session: Session, - smtpDestination: SmtpDestination, + fromAddress: String, + recipient: String, messageContent: MessageContent ): MimeMessage { // Create a new MimeMessage object val mimeMessage = MimeMessage(session) // Add from: - mimeMessage.setFrom(smtpDestination.fromAddress) + mimeMessage.setFrom(fromAddress) // Add to: - mimeMessage.setRecipients(Message.RecipientType.TO, smtpDestination.recipient) + mimeMessage.setRecipients(Message.RecipientType.TO, recipient) // Add Subject: mimeMessage.setSubject(messageContent.title, "UTF-8") @@ -119,7 +120,7 @@ internal object EmailMimeProvider { /** * Create a binary attachment part from channel attachment mimeMessage - * @param attachment channel attachment mimeMessage + * @param messageContent channel attachment mimeMessage * @return created mime body part for binary attachment */ private fun createBinaryAttachmentPart(messageContent: MessageContent): MimeBodyPart { @@ -135,7 +136,7 @@ internal object EmailMimeProvider { /** * Create a text attachment part from channel attachment mimeMessage - * @param attachment channel attachment mimeMessage + * @param messageContent channel attachment mimeMessage * @return created mime body part for text attachment */ private fun createTextAttachmentPart(messageContent: MessageContent): MimeBodyPart { diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/CredentialsProvider.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/CredentialsProvider.kt index 5b30dfb9..7ebe5dc6 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/CredentialsProvider.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/CredentialsProvider.kt @@ -12,8 +12,16 @@ package org.opensearch.notifications.spi.credentials import com.amazonaws.auth.AWSCredentialsProvider -import org.opensearch.notifications.spi.model.destination.SNSDestination +/** + * AWS Credential provider using region and/or role + */ interface CredentialsProvider { - fun getCredentialsProvider(destination: SNSDestination): AWSCredentialsProvider + /** + * create/get AWS Credential provider using region and/or role + * @param region AWS region + * @param roleArn optional role ARN + * @return AWSCredentialsProvider + */ + fun getCredentialsProvider(region: String, roleArn: String?): AWSCredentialsProvider } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SesClientFactory.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SesClientFactory.kt new file mode 100644 index 00000000..23ad2877 --- /dev/null +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SesClientFactory.kt @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.notifications.spi.credentials + +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.ses.SesClient + +/** + * Interface for creating SES client + */ +interface SesClientFactory { + fun createSesClient(region: Region, roleArn: String?): SesClient +} diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SNSClient.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SnsClientFactory.kt similarity index 72% rename from notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SNSClient.kt rename to notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SnsClientFactory.kt index 80c1e0e8..0f2c7f15 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SNSClient.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SnsClientFactory.kt @@ -12,8 +12,10 @@ package org.opensearch.notifications.spi.credentials import com.amazonaws.services.sns.AmazonSNS -import org.opensearch.notifications.spi.model.destination.SNSDestination -interface SNSClient { - fun getClient(destination: SNSDestination): AmazonSNS +/** + * Interface for creating SNS client + */ +interface SnsClientFactory { + fun createSnsClient(region: String, roleArn: String?): AmazonSNS } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/CredentialsProviderFactory.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/CredentialsProviderFactory.kt index 82a4effe..fb06254f 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/CredentialsProviderFactory.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/CredentialsProviderFactory.kt @@ -19,25 +19,24 @@ import com.amazonaws.auth.profile.ProfileCredentialsProvider import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder import com.amazonaws.services.securitytoken.model.AssumeRoleRequest import org.opensearch.notifications.spi.credentials.CredentialsProvider -import org.opensearch.notifications.spi.model.destination.SNSDestination class CredentialsProviderFactory : CredentialsProvider { - override fun getCredentialsProvider(destination: SNSDestination): AWSCredentialsProvider { - return if (destination.roleArn != null) { - getCredentialsProviderByIAMRole(destination) + override fun getCredentialsProvider(region: String, roleArn: String?): AWSCredentialsProvider { + return if (roleArn != null) { + getCredentialsProviderByIAMRole(region, roleArn) } else { DefaultAWSCredentialsProviderChain() } } - private fun getCredentialsProviderByIAMRole(destination: SNSDestination): AWSCredentialsProvider { + private fun getCredentialsProviderByIAMRole(region: String, roleArn: String?): AWSCredentialsProvider { // TODO cache credentials by role ARN? val stsClient = AWSSecurityTokenServiceClientBuilder.standard() .withCredentials(ProfileCredentialsProvider()) - .withRegion(destination.getRegion()) + .withRegion(region) .build() val roleRequest = AssumeRoleRequest() - .withRoleArn(destination.roleArn) + .withRoleArn(roleArn) .withRoleSessionName("opensearch-notifications") val roleResponse = stsClient.assumeRole(roleRequest) val sessionCredentials = roleResponse.credentials diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SNSClientFactory.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SNSClientFactory.kt deleted file mode 100644 index 27e296d9..00000000 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SNSClientFactory.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.notifications.spi.credentials.oss - -import com.amazonaws.services.sns.AmazonSNS -import com.amazonaws.services.sns.AmazonSNSClientBuilder -import org.opensearch.notifications.spi.credentials.SNSClient -import org.opensearch.notifications.spi.model.destination.SNSDestination - -class SNSClientFactory : SNSClient { - override fun getClient(destination: SNSDestination): AmazonSNS { - val credentials = CredentialsProviderFactory().getCredentialsProvider(destination) - return AmazonSNSClientBuilder.standard() - .withRegion(destination.getRegion()) - .withCredentials(credentials) - .build() - } -} diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SesClientFactoryImpl.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SesClientFactoryImpl.kt new file mode 100644 index 00000000..6b59658f --- /dev/null +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SesClientFactoryImpl.kt @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.notifications.spi.credentials.oss + +import org.opensearch.notifications.spi.credentials.SesClientFactory +import org.opensearch.notifications.spi.utils.SecurityAccess +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.ses.SesClient + +/** + * Factory for creating SES client + */ +object SesClientFactoryImpl : SesClientFactory { + override fun createSesClient(region: Region, roleArn: String?): SesClient { + return SecurityAccess.doPrivileged { + // TODO: use CredentialsProviderFactory when it supports AWS SDK v2 + SesClient.builder() + .region(region) + .credentialsProvider(DefaultCredentialsProvider.create()) + .build() + } + } +} diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SnsClientFactoryImpl.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SnsClientFactoryImpl.kt new file mode 100644 index 00000000..93003570 --- /dev/null +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SnsClientFactoryImpl.kt @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.spi.credentials.oss + +import com.amazonaws.services.sns.AmazonSNS +import com.amazonaws.services.sns.AmazonSNSClientBuilder +import org.opensearch.notifications.spi.credentials.SnsClientFactory +import org.opensearch.notifications.spi.utils.SecurityAccess + +/** + * Factory for creating SNS client + */ +object SnsClientFactoryImpl : SnsClientFactory { + override fun createSnsClient(region: String, roleArn: String?): AmazonSNS { + return SecurityAccess.doPrivileged { + val credentials = + CredentialsProviderFactory().getCredentialsProvider(region, roleArn) + AmazonSNSClientBuilder.standard() + .withRegion(region) + .withCredentials(credentials) + .build() + } + } +} diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SesDestination.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SesDestination.kt new file mode 100644 index 00000000..9a7f307e --- /dev/null +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SesDestination.kt @@ -0,0 +1,34 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.spi.model.destination + +import org.opensearch.common.Strings +import org.opensearch.notifications.spi.utils.validateEmail +import software.amazon.awssdk.regions.Region + +/** + * This class holds the contents of ses destination + */ +class SesDestination( + val awsRegion: String, + val roleArn: String?, + val fromAddress: String, + val recipient: String +) : BaseDestination(DestinationType.SES) { + + init { + require(!Strings.isNullOrEmpty(awsRegion)) { "aws region should be provided" } + require(Region.regions().any { it.id() == awsRegion }) { "aws region is not valid" } + validateEmail(fromAddress) + validateEmail(recipient) + } +} diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SNSDestination.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SnsDestination.kt similarity index 66% rename from notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SNSDestination.kt rename to notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SnsDestination.kt index 19ef096b..19432b13 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SNSDestination.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SnsDestination.kt @@ -13,16 +13,10 @@ package org.opensearch.notifications.spi.model.destination /** * This class holds the contents of SNS destination */ -data class SNSDestination( +data class SnsDestination( val topicArn: String, val roleArn: String? = null, ) : BaseDestination(DestinationType.SNS) { - - /** - * Get AWS region from topic arn - */ - fun getRegion(): String { - // sample topic arn arn:aws:sns:us-west-2:075315751589:test-notification - return topicArn.split(":".toRegex()).toTypedArray()[3] - } + // sample topic arn -> arn:aws:sns:us-west-2:075315751589:test-notification + val region: String = topicArn.split(":".toRegex()).toTypedArray()[3] } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransport.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransport.kt index 28598803..5ca1ebe2 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransport.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransport.kt @@ -24,11 +24,14 @@ internal interface DestinationTransport { /** * Sending notification message over this channel. * + * @param destination destination configuration for sending message * @param message The message to send notification + * @param referenceId referenceId for message * @return Channel message response */ fun sendMessage( destination: T, - message: MessageContent + message: MessageContent, + referenceId: String ): DestinationMessageResponse } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransportProvider.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransportProvider.kt index 6d19efa0..b1c538a1 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransportProvider.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransportProvider.kt @@ -23,16 +23,17 @@ internal object DestinationTransportProvider { private val webhookDestinationTransport = WebhookDestinationTransport() private val smtpDestinationTransport = SmtpDestinationTransport() - private val snsDestinationTransport = SNSDestinationTransport() + private val snsDestinationTransport = SnsDestinationTransport() + private val sesDestinationTransport = SesDestinationTransport() @OpenForTesting var destinationTransportMap = mapOf( - // TODO Add other destinations, ses DestinationType.SLACK to webhookDestinationTransport, DestinationType.CHIME to webhookDestinationTransport, DestinationType.CUSTOM_WEBHOOK to webhookDestinationTransport, DestinationType.SMTP to smtpDestinationTransport, - DestinationType.SNS to snsDestinationTransport + DestinationType.SNS to snsDestinationTransport, + DestinationType.SES to sesDestinationTransport ) /** diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SesDestinationTransport.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SesDestinationTransport.kt new file mode 100644 index 00000000..cc891d11 --- /dev/null +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SesDestinationTransport.kt @@ -0,0 +1,78 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.spi.transport + +import org.opensearch.notifications.spi.client.DestinationClientPool +import org.opensearch.notifications.spi.client.DestinationSesClient +import org.opensearch.notifications.spi.model.DestinationMessageResponse +import org.opensearch.notifications.spi.model.MessageContent +import org.opensearch.notifications.spi.model.destination.SesDestination +import org.opensearch.notifications.spi.utils.OpenForTesting +import org.opensearch.notifications.spi.utils.logger +import org.opensearch.rest.RestStatus +import java.io.IOException +import javax.mail.MessagingException +import javax.mail.internet.AddressException + +/** + * This class handles the client responsible for submitting the messages to all types of email destinations. + */ +internal class SesDestinationTransport : DestinationTransport { + + private val log by logger(SesDestinationTransport::class.java) + private val destinationEmailClient: DestinationSesClient + + constructor() { + this.destinationEmailClient = DestinationClientPool.sesClient + } + + @OpenForTesting + constructor(destinationSesClient: DestinationSesClient) { + this.destinationEmailClient = destinationSesClient + } + + override fun sendMessage( + destination: SesDestination, + message: MessageContent, + referenceId: String + ): DestinationMessageResponse { + return try { + destinationEmailClient.execute(destination, message, referenceId) + } catch (addressException: AddressException) { + log.error("Error sending Email: recipient parsing failed with status:${addressException.message}") + DestinationMessageResponse( + RestStatus.BAD_REQUEST.status, + "recipient parsing failed with status:${addressException.message}" + ) + } catch (messagingException: MessagingException) { + log.error("Error sending Email: Email message creation failed with status:${messagingException.message}") + DestinationMessageResponse( + RestStatus.FAILED_DEPENDENCY.status, + "Email message creation failed with status:${messagingException.message}" + ) + } catch (ioException: IOException) { + log.error("Error sending Email: Email message creation failed with status:${ioException.message}") + DestinationMessageResponse( + RestStatus.FAILED_DEPENDENCY.status, + "Email message creation failed with status:${ioException.message}" + ) + } catch (illegalArgumentException: IllegalArgumentException) { + log.error( + "Error sending Email: Email message creation failed with status:${illegalArgumentException.message}" + ) + DestinationMessageResponse( + RestStatus.BAD_REQUEST.status, + "Email message creation failed with status:${illegalArgumentException.message}" + ) + } + } +} diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SmtpDestinationTransport.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SmtpDestinationTransport.kt index 813040a8..0cb30f71 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SmtpDestinationTransport.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SmtpDestinationTransport.kt @@ -40,9 +40,13 @@ internal class SmtpDestinationTransport : DestinationTransport this.destinationEmailClient = destinationSmtpClient } - override fun sendMessage(destination: SmtpDestination, message: MessageContent): DestinationMessageResponse { + override fun sendMessage( + destination: SmtpDestination, + message: MessageContent, + referenceId: String + ): DestinationMessageResponse { return try { - destinationEmailClient.execute(destination, message) + destinationEmailClient.execute(destination, message, referenceId) } catch (addressException: AddressException) { log.error("Error sending Email: recipient parsing failed with status:${addressException.message}") DestinationMessageResponse( @@ -66,7 +70,7 @@ internal class SmtpDestinationTransport : DestinationTransport "Error sending Email: Email message creation failed with status:${illegalArgumentException.message}" ) DestinationMessageResponse( - RestStatus.FAILED_DEPENDENCY.status, + RestStatus.BAD_REQUEST.status, "Email message creation failed with status:${illegalArgumentException.message}" ) } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SNSDestinationTransport.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SnsDestinationTransport.kt similarity index 50% rename from notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SNSDestinationTransport.kt rename to notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SnsDestinationTransport.kt index a63c390a..5cffeb91 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SNSDestinationTransport.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SnsDestinationTransport.kt @@ -12,9 +12,11 @@ package org.opensearch.notifications.spi.transport import org.opensearch.notifications.spi.client.DestinationClientPool +import org.opensearch.notifications.spi.client.DestinationSnsClient import org.opensearch.notifications.spi.model.DestinationMessageResponse import org.opensearch.notifications.spi.model.MessageContent -import org.opensearch.notifications.spi.model.destination.SNSDestination +import org.opensearch.notifications.spi.model.destination.SnsDestination +import org.opensearch.notifications.spi.utils.OpenForTesting import org.opensearch.notifications.spi.utils.logger import org.opensearch.rest.RestStatus import java.io.IOException @@ -22,17 +24,30 @@ import java.io.IOException /** * This class handles the client responsible for submitting the messages to SNS destinations. */ -internal class SNSDestinationTransport : DestinationTransport { +internal class SnsDestinationTransport : DestinationTransport { - private val log by logger(SNSDestinationTransport::class.java) + private val log by logger(SnsDestinationTransport::class.java) + private val destinationSNSClient: DestinationSnsClient - override fun sendMessage(destination: SNSDestination, message: MessageContent): DestinationMessageResponse { + constructor() { + this.destinationSNSClient = DestinationClientPool.snsClient + } + + @OpenForTesting + constructor(destinationSmtpClient: DestinationSnsClient) { + this.destinationSNSClient = destinationSmtpClient + } + + override fun sendMessage( + destination: SnsDestination, + message: MessageContent, + referenceId: String + ): DestinationMessageResponse { return try { - val snsClient = DestinationClientPool.getSNSClient(destination) - val response = snsClient.execute(destination.topicArn, message) + val response = destinationSNSClient.execute(destination, message, referenceId) DestinationMessageResponse(RestStatus.OK.status, "Success, message id: $response") - } catch (exception: IOException) { - log.error("Exception sending message: $message", exception) + } catch (exception: IOException) { // TODO:Add specific SNS exception and throw corresponding errors + log.error("Exception sending message id $referenceId", exception) DestinationMessageResponse( RestStatus.INTERNAL_SERVER_ERROR.status, "Failed to send message ${exception.message}" diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/WebhookDestinationTransport.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/WebhookDestinationTransport.kt index cdca28b0..4225065c 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/WebhookDestinationTransport.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/WebhookDestinationTransport.kt @@ -38,12 +38,16 @@ internal class WebhookDestinationTransport : DestinationTransport Date: Thu, 12 Aug 2021 14:16:34 -0700 Subject: [PATCH 15/29] Disallow inconsistent config types in update requests (#280) Signed-off-by: Joshua Li --- .../index/ConfigIndexingActions.kt | 4 ++ .../config/ChimeNotificationConfigCrudIT.kt | 61 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt index b1bc77df..40ecc24e 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt @@ -238,6 +238,10 @@ object ConfigIndexingActions { RestStatus.FORBIDDEN ) } + if (currentConfigDoc.configDoc.config.configType != request.notificationConfig.configType) { + throw OpenSearchStatusException("Config type cannot be changed after creation", RestStatus.CONFLICT) + } + val newMetadata = currentMetadata.copy(lastUpdateTime = Instant.now()) val newConfigData = NotificationConfigDoc(newMetadata, request.notificationConfig) if (!operations.updateNotificationConfig(request.configId, newConfigData)) { diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/ChimeNotificationConfigCrudIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/ChimeNotificationConfigCrudIT.kt index 40149453..7a3079a2 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/ChimeNotificationConfigCrudIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/ChimeNotificationConfigCrudIT.kt @@ -203,4 +203,65 @@ class ChimeNotificationConfigCrudIT : PluginRestTestCase() { RestStatus.BAD_REQUEST.status ) } + + fun `test update existing config to different config type`() { + // Create sample config request reference + val sampleChime = Chime("https://domain.com/sample_chime_url#1234567890") + val referenceObject = NotificationConfig( + "this is a sample config name", + "this is a sample config description", + ConfigType.CHIME, + EnumSet.of(Feature.ALERTING, Feature.REPORTS), + isEnabled = true, + configData = sampleChime + ) + + // Create chime notification config + val createRequestJsonString = """ + { + "config":{ + "name":"${referenceObject.name}", + "description":"${referenceObject.description}", + "config_type":"chime", + "feature_list":[ + "${referenceObject.features.elementAt(0)}", + "${referenceObject.features.elementAt(1)}" + ], + "is_enabled":${referenceObject.isEnabled}, + "chime":{"url":"${(referenceObject.configData as Chime).url}"} + } + } + """.trimIndent() + val createResponse = executeRequest( + RestRequest.Method.POST.name, + "$PLUGIN_BASE_URI/configs", + createRequestJsonString, + RestStatus.OK.status + ) + val configId = createResponse.get("config_id").asString + Assert.assertNotNull(configId) + Thread.sleep(1000) + + // Update to slack notification config + val updateRequestJsonString = """ + { + "config":{ + "name":"this is a updated config name", + "description":"this is a updated config description", + "config_type":"slack", + "feature_list":[ + "${Feature.INDEX_MANAGEMENT}" + ], + "is_enabled":"true", + "slack":{"url":"https://updated.domain.com/updated_slack_url#0987654321"} + } + } + """.trimIndent() + executeRequest( + RestRequest.Method.PUT.name, + "$PLUGIN_BASE_URI/configs/$configId", + updateRequestJsonString, + RestStatus.CONFLICT.status + ) + } } From 3cc9fa459bb5b88422fd6e7fd12d42ae312412c0 Mon Sep 17 00:00:00 2001 From: Anantha Krishna Bhatta Date: Thu, 12 Aug 2021 18:27:32 -0700 Subject: [PATCH 16/29] Refactor Feature from enum to string [Tests] Updated tests to use String Signed-off-by: @akbhatta --- .../action/GetFeatureChannelListAction.kt | 7 +--- .../index/ConfigIndexingActions.kt | 19 +++++---- .../notifications/index/ConfigQueryHelper.kt | 8 ++-- ...tificationFeatureChannelListRestHandler.kt | 3 +- .../resthandler/SendTestMessageRestHandler.kt | 15 ++++--- .../send/SendMessageActionHelper.kt | 9 ++-- .../config/ChimeNotificationConfigCrudIT.kt | 15 +++---- .../config/CreateNotificationConfigIT.kt | 17 ++++---- .../config/EmailNotificationConfigCrudIT.kt | 26 ++++++------ .../config/QueryNotificationConfigIT.kt | 42 +++++++++---------- .../config/SlackNotificationConfigCrudIT.kt | 14 +++---- .../config/WebhookNotificationConfigCrudIT.kt | 11 ++--- .../GetNotificationFeatureChannelListIT.kt | 23 ++++------ .../notifications/ObjectEqualsHelpers.kt | 3 +- .../model/NotificationConfigDocTests.kt | 7 ++-- .../model/NotificationEventDocTests.kt | 6 +-- 16 files changed, 109 insertions(+), 116 deletions(-) diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/GetFeatureChannelListAction.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/GetFeatureChannelListAction.kt index f8f9d92a..1694270b 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/GetFeatureChannelListAction.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/GetFeatureChannelListAction.kt @@ -31,18 +31,17 @@ import org.opensearch.action.ActionListener import org.opensearch.action.ActionRequest import org.opensearch.action.support.ActionFilters import org.opensearch.client.Client +import org.opensearch.common.Strings import org.opensearch.common.inject.Inject import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.commons.authuser.User import org.opensearch.commons.notifications.action.GetFeatureChannelListRequest import org.opensearch.commons.notifications.action.GetFeatureChannelListResponse import org.opensearch.commons.notifications.action.NotificationsActions -import org.opensearch.commons.notifications.model.Feature import org.opensearch.commons.utils.recreateObject import org.opensearch.notifications.index.ConfigIndexingActions import org.opensearch.tasks.Task import org.opensearch.transport.TransportService -import java.lang.IllegalArgumentException /** * Get feature channel list transport action @@ -81,9 +80,7 @@ internal class GetFeatureChannelListAction @Inject constructor( request: GetFeatureChannelListRequest, user: User? ): GetFeatureChannelListResponse { - if (request.feature == Feature.NONE) { - throw IllegalArgumentException("Not a valid feature") - } + require(!Strings.isNullOrEmpty(request.feature)) { "Not a valid feature" } // TODO: Validate against allowed features return ConfigIndexingActions.getFeatureChannelList(request, user) } } diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt index 40ecc24e..1c3efee9 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt @@ -43,15 +43,15 @@ import org.opensearch.commons.notifications.model.Chime import org.opensearch.commons.notifications.model.ConfigType import org.opensearch.commons.notifications.model.Email import org.opensearch.commons.notifications.model.EmailGroup -import org.opensearch.commons.notifications.model.Feature import org.opensearch.commons.notifications.model.FeatureChannel import org.opensearch.commons.notifications.model.FeatureChannelList import org.opensearch.commons.notifications.model.NotificationConfig import org.opensearch.commons.notifications.model.NotificationConfigInfo import org.opensearch.commons.notifications.model.NotificationConfigSearchResult -import org.opensearch.commons.notifications.model.SNS +import org.opensearch.commons.notifications.model.SesAccount import org.opensearch.commons.notifications.model.Slack import org.opensearch.commons.notifications.model.SmtpAccount +import org.opensearch.commons.notifications.model.Sns import org.opensearch.commons.notifications.model.Webhook import org.opensearch.commons.utils.logger import org.opensearch.notifications.NotificationPlugin.Companion.LOG_PREFIX @@ -60,7 +60,6 @@ import org.opensearch.notifications.model.NotificationConfigDoc import org.opensearch.notifications.security.UserAccess import org.opensearch.rest.RestStatus import java.time.Instant -import java.util.EnumSet /** * NotificationConfig indexing operation actions. @@ -93,11 +92,11 @@ object ConfigIndexingActions { } @Suppress("UnusedPrivateMember") - private fun validateSnsConfig(sns: SNS, user: User?) { + private fun validateSnsConfig(sns: Sns, user: User?) { // TODO: URL validation with rules } - private fun validateEmailConfig(email: Email, features: EnumSet, user: User?) { + private fun validateEmailConfig(email: Email, features: Set, user: User?) { if (email.emailGroupIds.contains(email.emailAccountID)) { throw OpenSearchStatusException( "Config IDs ${email.emailAccountID} is in both emailAccountID and emailGroupIds", @@ -165,6 +164,11 @@ object ConfigIndexingActions { // TODO: host validation with rules } + @Suppress("UnusedPrivateMember") + private fun validateSesAccountConfig(sesAccount: SesAccount, user: User?) { + // TODO: host validation with rules + } + @Suppress("UnusedPrivateMember") private fun validateEmailGroupConfig(emailGroup: EmailGroup, user: User?) { // No extra validation required. All email IDs are validated as part of model validation. @@ -181,8 +185,9 @@ object ConfigIndexingActions { ConfigType.WEBHOOK -> validateWebhookConfig(config.configData as Webhook, user) ConfigType.EMAIL -> validateEmailConfig(config.configData as Email, config.features, user) ConfigType.SMTP_ACCOUNT -> validateSmtpAccountConfig(config.configData as SmtpAccount, user) + ConfigType.SES_ACCOUNT -> validateSesAccountConfig(config.configData as SesAccount, user) ConfigType.EMAIL_GROUP -> validateEmailGroupConfig(config.configData as EmailGroup, user) - ConfigType.SNS -> validateSnsConfig(config.configData as SNS, user) + ConfigType.SNS -> validateSnsConfig(config.configData as Sns, user) } } @@ -358,7 +363,7 @@ object ConfigIndexingActions { userAccess.validateUser(user) val supportedChannelListString = getSupportedChannelList().joinToString(",") val filterParams = mapOf( - Pair("feature_list", request.feature.tag), + Pair("feature_list", request.feature), Pair("config_type", supportedChannelListString) ) val getAllRequest = GetNotificationConfigRequest(filterParams = filterParams) diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigQueryHelper.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigQueryHelper.kt index 7aeb5524..97f8108c 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigQueryHelper.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigQueryHelper.kt @@ -41,8 +41,8 @@ import org.opensearch.commons.notifications.NotificationConstants.METHOD_TAG import org.opensearch.commons.notifications.NotificationConstants.NAME_TAG import org.opensearch.commons.notifications.NotificationConstants.QUERY_TAG import org.opensearch.commons.notifications.NotificationConstants.RECIPIENT_LIST_TAG -import org.opensearch.commons.notifications.NotificationConstants.ROLE_ARN_FIELD -import org.opensearch.commons.notifications.NotificationConstants.TOPIC_ARN_FIELD +import org.opensearch.commons.notifications.NotificationConstants.ROLE_ARN_TAG +import org.opensearch.commons.notifications.NotificationConstants.TOPIC_ARN_TAG import org.opensearch.commons.notifications.NotificationConstants.UPDATED_TIME_TAG import org.opensearch.commons.notifications.NotificationConstants.URL_TAG import org.opensearch.commons.notifications.model.ConfigType.CHIME @@ -88,8 +88,8 @@ object ConfigQueryHelper { "${SMTP_ACCOUNT.tag}.$HOST_TAG", "${SMTP_ACCOUNT.tag}.$FROM_ADDRESS_TAG", "${EMAIL_GROUP.tag}.$RECIPIENT_LIST_TAG", - "${SNS.tag}.$TOPIC_ARN_FIELD", - "${SNS.tag}.$ROLE_ARN_FIELD" + "${SNS.tag}.$TOPIC_ARN_TAG", + "${SNS.tag}.$ROLE_ARN_TAG" ) private val METADATA_FIELDS = METADATA_RANGE_FIELDS diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationFeatureChannelListRestHandler.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationFeatureChannelListRestHandler.kt index f8e56907..6f99f181 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationFeatureChannelListRestHandler.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationFeatureChannelListRestHandler.kt @@ -15,7 +15,6 @@ import org.opensearch.client.node.NodeClient import org.opensearch.commons.notifications.NotificationConstants.FEATURE_TAG import org.opensearch.commons.notifications.NotificationsPluginInterface import org.opensearch.commons.notifications.action.GetFeatureChannelListRequest -import org.opensearch.commons.notifications.model.Feature import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI import org.opensearch.rest.BaseRestHandler.RestChannelConsumer import org.opensearch.rest.BytesRestResponse @@ -71,7 +70,7 @@ internal class NotificationFeatureChannelListRestHandler : PluginBaseHandler() { override fun executeRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { return when (request.method()) { GET -> { - val feature = Feature.fromTagOrDefault(request.param(FEATURE_TAG)) + val feature = request.param(FEATURE_TAG) RestChannelConsumer { NotificationsPluginInterface.getFeatureChannelList( client, diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/SendTestMessageRestHandler.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/SendTestMessageRestHandler.kt index 3795f73b..a00a97ec 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/SendTestMessageRestHandler.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/SendTestMessageRestHandler.kt @@ -17,7 +17,6 @@ import org.opensearch.commons.notifications.NotificationConstants.FEATURE_TAG import org.opensearch.commons.notifications.NotificationsPluginInterface import org.opensearch.commons.notifications.model.ChannelMessage import org.opensearch.commons.notifications.model.EventSource -import org.opensearch.commons.notifications.model.Feature import org.opensearch.commons.notifications.model.SeverityType import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI import org.opensearch.rest.BaseRestHandler.RestChannelConsumer @@ -84,7 +83,7 @@ internal class SendTestMessageRestHandler : PluginBaseHandler() { request: RestRequest, client: NodeClient ) = RestChannelConsumer { - val feature = Feature.fromTagOrDefault(request.param(FEATURE_TAG, Feature.NONE.tag)) + val feature = request.param(FEATURE_TAG) val configId = request.param(CONFIG_ID_TAG) val source = generateEventSource(feature, configId) val message = ChannelMessage( @@ -102,7 +101,7 @@ internal class SendTestMessageRestHandler : PluginBaseHandler() { ) } - private fun generateEventSource(feature: Feature, configId: String): EventSource { + private fun generateEventSource(feature: String, configId: String): EventSource { return EventSource( getMessageTitle(feature, configId), configId, @@ -111,20 +110,20 @@ internal class SendTestMessageRestHandler : PluginBaseHandler() { ) } - private fun getMessageTitle(feature: Feature, configId: String): String { + private fun getMessageTitle(feature: String, configId: String): String { return "[$feature] Test Message Title-$configId" // TODO: change as spec } - private fun getMessageTextDescription(feature: Feature, configId: String): String { - return "Test message content body for config id $configId\nfrom feature ${feature.tag}" // TODO: change as spec + private fun getMessageTextDescription(feature: String, configId: String): String { + return "Test message content body for config id $configId\nfrom feature $feature" // TODO: change as spec } - private fun getMessageHtmlDescription(feature: Feature, configId: String): String { + private fun getMessageHtmlDescription(feature: String, configId: String): String { return """
Test Message
-

Test Message for config id $configId from feature ${feature.tag}

+

Test Message for config id $configId from feature $feature

""".trimIndent() // TODO: change as spec 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 559032ac..f4ecd5e1 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 @@ -28,9 +28,9 @@ import org.opensearch.commons.notifications.model.EmailRecipientStatus import org.opensearch.commons.notifications.model.EventSource import org.opensearch.commons.notifications.model.EventStatus import org.opensearch.commons.notifications.model.NotificationEvent -import org.opensearch.commons.notifications.model.SNS import org.opensearch.commons.notifications.model.Slack import org.opensearch.commons.notifications.model.SmtpAccount +import org.opensearch.commons.notifications.model.Sns import org.opensearch.commons.notifications.model.Webhook import org.opensearch.commons.utils.logger import org.opensearch.notifications.NotificationPlugin.Companion.LOG_PREFIX @@ -191,9 +191,10 @@ object SendMessageActionHelper { eventStatus, eventSource.referenceId ) + ConfigType.SES_ACCOUNT -> null // TODO : Implement ConfigType.SMTP_ACCOUNT -> null ConfigType.EMAIL_GROUP -> null - ConfigType.SNS -> sendSNSMessage(configData as SNS, message, eventStatus, eventSource.referenceId) + ConfigType.SNS -> sendSNSMessage(configData as Sns, message, eventStatus, eventSource.referenceId) } return if (response == null) { log.warn("Cannot send message to destination for config id :${channel.docInfo.id}") @@ -341,12 +342,12 @@ object SendMessageActionHelper { * send message to SNS destination */ private fun sendSNSMessage( - sns: SNS, + sns: Sns, message: MessageContent, eventStatus: EventStatus, referenceId: String ): EventStatus { - val destination = SnsDestination(sns.topicARN, sns.roleARN) + val destination = SnsDestination(sns.topicArn, sns.roleArn) val status = sendMessageThroughSpi(destination, message, referenceId) return eventStatus.copy(deliveryStatus = DeliveryStatus(status.statusCode.toString(), status.statusText)) } diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/ChimeNotificationConfigCrudIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/ChimeNotificationConfigCrudIT.kt index 7a3079a2..1263f6f8 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/ChimeNotificationConfigCrudIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/ChimeNotificationConfigCrudIT.kt @@ -27,16 +27,17 @@ package org.opensearch.integtest.config import org.junit.Assert +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_ALERTING +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_INDEX_MANAGEMENT +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_REPORTS import org.opensearch.commons.notifications.model.Chime import org.opensearch.commons.notifications.model.ConfigType -import org.opensearch.commons.notifications.model.Feature import org.opensearch.commons.notifications.model.NotificationConfig import org.opensearch.integtest.PluginRestTestCase import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI import org.opensearch.notifications.verifySingleConfigEquals import org.opensearch.rest.RestRequest import org.opensearch.rest.RestStatus -import java.util.EnumSet class ChimeNotificationConfigCrudIT : PluginRestTestCase() { @@ -47,7 +48,7 @@ class ChimeNotificationConfigCrudIT : PluginRestTestCase() { "this is a sample config name", "this is a sample config description", ConfigType.CHIME, - EnumSet.of(Feature.ALERTING, Feature.REPORTS), + setOf(FEATURE_ALERTING, FEATURE_REPORTS), isEnabled = true, configData = sampleChime ) @@ -106,7 +107,7 @@ class ChimeNotificationConfigCrudIT : PluginRestTestCase() { "this is a updated config name", "this is a updated config description", ConfigType.CHIME, - EnumSet.of(Feature.INDEX_MANAGEMENT), + setOf(FEATURE_INDEX_MANAGEMENT), isEnabled = true, configData = updatedChime ) @@ -174,7 +175,7 @@ class ChimeNotificationConfigCrudIT : PluginRestTestCase() { "this is a sample config name", "this is a sample config description", ConfigType.CHIME, - EnumSet.of(Feature.ALERTING, Feature.REPORTS), + setOf(FEATURE_ALERTING, FEATURE_REPORTS), isEnabled = true, configData = sampleChime ) @@ -211,7 +212,7 @@ class ChimeNotificationConfigCrudIT : PluginRestTestCase() { "this is a sample config name", "this is a sample config description", ConfigType.CHIME, - EnumSet.of(Feature.ALERTING, Feature.REPORTS), + setOf(FEATURE_ALERTING, FEATURE_REPORTS), isEnabled = true, configData = sampleChime ) @@ -250,7 +251,7 @@ class ChimeNotificationConfigCrudIT : PluginRestTestCase() { "description":"this is a updated config description", "config_type":"slack", "feature_list":[ - "${Feature.INDEX_MANAGEMENT}" + "$FEATURE_INDEX_MANAGEMENT" ], "is_enabled":"true", "slack":{"url":"https://updated.domain.com/updated_slack_url#0987654321"} diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/CreateNotificationConfigIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/CreateNotificationConfigIT.kt index 3d4044d4..2dcd530a 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/CreateNotificationConfigIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/CreateNotificationConfigIT.kt @@ -28,9 +28,11 @@ package org.opensearch.integtest.config import org.junit.Assert +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_ALERTING +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_INDEX_MANAGEMENT +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_REPORTS import org.opensearch.commons.notifications.model.Chime import org.opensearch.commons.notifications.model.ConfigType -import org.opensearch.commons.notifications.model.Feature import org.opensearch.commons.notifications.model.MethodType import org.opensearch.commons.notifications.model.NotificationConfig import org.opensearch.commons.notifications.model.Slack @@ -41,7 +43,6 @@ import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI import org.opensearch.notifications.verifySingleConfigEquals import org.opensearch.rest.RestRequest import org.opensearch.rest.RestStatus -import java.util.EnumSet class CreateNotificationConfigIT : PluginRestTestCase() { @@ -52,7 +53,7 @@ class CreateNotificationConfigIT : PluginRestTestCase() { "this is a sample config name", "this is a sample config description", ConfigType.SLACK, - EnumSet.of(Feature.INDEX_MANAGEMENT, Feature.REPORTS), + setOf(FEATURE_INDEX_MANAGEMENT, FEATURE_REPORTS), isEnabled = true, configData = sampleSlack ) @@ -83,7 +84,7 @@ class CreateNotificationConfigIT : PluginRestTestCase() { Assert.assertNotNull(configId) Thread.sleep(1000) - // Get slack notification config + // Get Slack notification config val getConfigResponse = executeRequest( RestRequest.Method.GET.name, @@ -102,7 +103,7 @@ class CreateNotificationConfigIT : PluginRestTestCase() { "this is a sample config name", "this is a sample config description", ConfigType.CHIME, - EnumSet.of(Feature.ALERTING, Feature.REPORTS), + setOf(FEATURE_ALERTING, FEATURE_REPORTS), isEnabled = true, configData = sampleChime ) @@ -151,7 +152,7 @@ class CreateNotificationConfigIT : PluginRestTestCase() { "this is a sample config name", "this is a sample config description", ConfigType.WEBHOOK, - EnumSet.of(Feature.INDEX_MANAGEMENT, Feature.REPORTS, Feature.ALERTING), + setOf(FEATURE_INDEX_MANAGEMENT, FEATURE_REPORTS, FEATURE_ALERTING), isEnabled = true, configData = sampleWebhook ) @@ -200,7 +201,7 @@ class CreateNotificationConfigIT : PluginRestTestCase() { "this is another config name", "this is another config description", ConfigType.WEBHOOK, - EnumSet.of(Feature.INDEX_MANAGEMENT, Feature.REPORTS), + setOf(FEATURE_INDEX_MANAGEMENT, FEATURE_REPORTS), isEnabled = true, configData = anotherWebhook ) @@ -244,7 +245,7 @@ class CreateNotificationConfigIT : PluginRestTestCase() { "this is a sample smtp account config name", "this is a sample smtp account config description", ConfigType.SMTP_ACCOUNT, - EnumSet.of(Feature.REPORTS), + setOf(FEATURE_REPORTS), isEnabled = true, configData = sampleSmtpAccount ) diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/EmailNotificationConfigCrudIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/EmailNotificationConfigCrudIT.kt index bc1382b1..5e249a34 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/EmailNotificationConfigCrudIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/EmailNotificationConfigCrudIT.kt @@ -28,10 +28,11 @@ package org.opensearch.integtest.config import org.junit.Assert +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_INDEX_MANAGEMENT +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_REPORTS import org.opensearch.commons.notifications.model.ConfigType import org.opensearch.commons.notifications.model.Email import org.opensearch.commons.notifications.model.EmailGroup -import org.opensearch.commons.notifications.model.Feature import org.opensearch.commons.notifications.model.MethodType import org.opensearch.commons.notifications.model.NotificationConfig import org.opensearch.commons.notifications.model.SmtpAccount @@ -43,7 +44,6 @@ import org.opensearch.notifications.verifySingleConfigEquals import org.opensearch.notifications.verifySingleConfigIdEquals import org.opensearch.rest.RestRequest import org.opensearch.rest.RestStatus -import java.util.EnumSet class EmailNotificationConfigCrudIT : PluginRestTestCase() { @@ -59,7 +59,7 @@ class EmailNotificationConfigCrudIT : PluginRestTestCase() { "this is a sample smtp account config name", "this is a sample smtp account config description", ConfigType.SMTP_ACCOUNT, - EnumSet.of(Feature.REPORTS), + setOf(FEATURE_REPORTS), isEnabled = true, configData = sampleSmtpAccount ) @@ -100,7 +100,7 @@ class EmailNotificationConfigCrudIT : PluginRestTestCase() { "this is a sample email group config name", "this is a sample email group config description", ConfigType.EMAIL_GROUP, - EnumSet.of(Feature.REPORTS), + setOf(FEATURE_REPORTS), isEnabled = true, configData = sampleEmailGroup ) @@ -145,7 +145,7 @@ class EmailNotificationConfigCrudIT : PluginRestTestCase() { "this is a sample config name", "this is a sample config description", ConfigType.EMAIL, - EnumSet.of(Feature.REPORTS), + setOf(FEATURE_REPORTS), isEnabled = true, configData = sampleEmail ) @@ -241,7 +241,7 @@ class EmailNotificationConfigCrudIT : PluginRestTestCase() { "this is a updated smtp account config name", "this is a updated smtp account config description", ConfigType.SMTP_ACCOUNT, - EnumSet.of(Feature.REPORTS), + setOf(FEATURE_REPORTS), isEnabled = true, configData = updatedSmtpAccount ) @@ -367,7 +367,7 @@ class EmailNotificationConfigCrudIT : PluginRestTestCase() { "this is a sample config name", "this is a sample config description", ConfigType.EMAIL, - EnumSet.of(Feature.REPORTS), + setOf(FEATURE_REPORTS), isEnabled = true, configData = sampleEmail ) @@ -425,7 +425,7 @@ class EmailNotificationConfigCrudIT : PluginRestTestCase() { "this is a sample config name", "this is a sample config description", ConfigType.EMAIL, - EnumSet.of(Feature.REPORTS), + setOf(FEATURE_REPORTS), isEnabled = true, configData = sampleEmail ) @@ -483,7 +483,7 @@ class EmailNotificationConfigCrudIT : PluginRestTestCase() { "this is a sample smtp account config name", "this is a sample smtp account config description", ConfigType.SMTP_ACCOUNT, - EnumSet.of(Feature.REPORTS), + setOf(FEATURE_REPORTS), isEnabled = true, configData = sampleSmtpAccount ) @@ -528,7 +528,7 @@ class EmailNotificationConfigCrudIT : PluginRestTestCase() { "this is a sample config name", "this is a sample config description", ConfigType.EMAIL, - EnumSet.of(Feature.REPORTS), + setOf(FEATURE_REPORTS), isEnabled = true, configData = sampleEmail ) @@ -618,7 +618,7 @@ class EmailNotificationConfigCrudIT : PluginRestTestCase() { "this is a sample config name", "this is a sample config description", ConfigType.EMAIL, - EnumSet.of(Feature.REPORTS, Feature.INDEX_MANAGEMENT), + setOf(FEATURE_REPORTS, FEATURE_INDEX_MANAGEMENT), isEnabled = true, configData = sampleEmail ) @@ -881,7 +881,7 @@ class EmailNotificationConfigCrudIT : PluginRestTestCase() { "this is a sample smtp account config name", "this is a sample smtp account config description", ConfigType.SMTP_ACCOUNT, - EnumSet.of(Feature.REPORTS), + setOf(FEATURE_REPORTS), isEnabled = true, configData = sampleSmtpAccount ) @@ -926,7 +926,7 @@ class EmailNotificationConfigCrudIT : PluginRestTestCase() { "this is a sample config name", "this is a sample config description", ConfigType.EMAIL, - EnumSet.of(Feature.REPORTS), + setOf(FEATURE_REPORTS), isEnabled = true, configData = sampleEmail ) diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/QueryNotificationConfigIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/QueryNotificationConfigIT.kt index dbdfff1a..58053983 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/QueryNotificationConfigIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/QueryNotificationConfigIT.kt @@ -28,12 +28,11 @@ package org.opensearch.integtest.config import org.junit.Assert +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_ALERTING +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_INDEX_MANAGEMENT +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_REPORTS import org.opensearch.commons.notifications.model.Chime import org.opensearch.commons.notifications.model.ConfigType -import org.opensearch.commons.notifications.model.Feature -import org.opensearch.commons.notifications.model.Feature.ALERTING -import org.opensearch.commons.notifications.model.Feature.INDEX_MANAGEMENT -import org.opensearch.commons.notifications.model.Feature.REPORTS import org.opensearch.commons.notifications.model.NotificationConfig import org.opensearch.integtest.PluginRestTestCase import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI @@ -44,7 +43,6 @@ import org.opensearch.notifications.verifySingleConfigIdEquals import org.opensearch.rest.RestRequest import org.opensearch.rest.RestStatus import java.time.Instant -import java.util.EnumSet import kotlin.random.Random class QueryNotificationConfigIT : PluginRestTestCase() { @@ -54,7 +52,7 @@ class QueryNotificationConfigIT : PluginRestTestCase() { nameSubstring: String, configType: ConfigType, isEnabled: Boolean, - features: Set + features: Set ): String { val randomString = (1..20) .map { Random.nextInt(0, charPool.size) } @@ -108,7 +106,7 @@ class QueryNotificationConfigIT : PluginRestTestCase() { nameSubstring: String = "", configType: ConfigType = ConfigType.SLACK, isEnabled: Boolean = true, - features: Set = setOf(ALERTING, INDEX_MANAGEMENT, Feature.REPORTS) + features: Set = setOf(FEATURE_ALERTING, FEATURE_INDEX_MANAGEMENT, FEATURE_REPORTS) ): String { val createRequestJsonString = getCreateRequestJsonString(nameSubstring, configType, isEnabled, features) val createResponse = executeRequest( @@ -401,13 +399,13 @@ class QueryNotificationConfigIT : PluginRestTestCase() { } fun `test Get sorted notification config using multi keyword sort_field(features)`() { - val iId = createConfig(features = setOf(INDEX_MANAGEMENT)) - val aId = createConfig(features = setOf(ALERTING)) - val rId = createConfig(features = setOf(REPORTS)) - val iaId = createConfig(features = setOf(INDEX_MANAGEMENT, ALERTING)) - val raId = createConfig(features = setOf(REPORTS, ALERTING)) - val riId = createConfig(features = setOf(REPORTS, INDEX_MANAGEMENT)) - val iarId = createConfig(features = setOf(INDEX_MANAGEMENT, ALERTING, REPORTS)) + val iId = createConfig(features = setOf(FEATURE_INDEX_MANAGEMENT)) + val aId = createConfig(features = setOf(FEATURE_ALERTING)) + val rId = createConfig(features = setOf(FEATURE_REPORTS)) + val iaId = createConfig(features = setOf(FEATURE_INDEX_MANAGEMENT, FEATURE_ALERTING)) + val raId = createConfig(features = setOf(FEATURE_REPORTS, FEATURE_ALERTING)) + val riId = createConfig(features = setOf(FEATURE_REPORTS, FEATURE_INDEX_MANAGEMENT)) + val iarId = createConfig(features = setOf(FEATURE_INDEX_MANAGEMENT, FEATURE_ALERTING, FEATURE_REPORTS)) Thread.sleep(1000) val sortedConfigIds = listOf(aId, iaId, raId, iarId, iId, riId, rId) @@ -576,13 +574,13 @@ class QueryNotificationConfigIT : PluginRestTestCase() { } fun `test Get filtered notification config using keyword filter_param_list(features)`() { - val iId = createConfig(features = setOf(INDEX_MANAGEMENT)) - val aId = createConfig(features = setOf(ALERTING)) - val rId = createConfig(features = setOf(REPORTS)) - val iaId = createConfig(features = setOf(INDEX_MANAGEMENT, ALERTING)) - val raId = createConfig(features = setOf(REPORTS, ALERTING)) - val riId = createConfig(features = setOf(REPORTS, INDEX_MANAGEMENT)) - val iarId = createConfig(features = setOf(INDEX_MANAGEMENT, ALERTING, REPORTS)) + val iId = createConfig(features = setOf(FEATURE_INDEX_MANAGEMENT)) + val aId = createConfig(features = setOf(FEATURE_ALERTING)) + val rId = createConfig(features = setOf(FEATURE_REPORTS)) + val iaId = createConfig(features = setOf(FEATURE_INDEX_MANAGEMENT, FEATURE_ALERTING)) + val raId = createConfig(features = setOf(FEATURE_REPORTS, FEATURE_ALERTING)) + val riId = createConfig(features = setOf(FEATURE_REPORTS, FEATURE_INDEX_MANAGEMENT)) + val iarId = createConfig(features = setOf(FEATURE_INDEX_MANAGEMENT, FEATURE_ALERTING, FEATURE_REPORTS)) Thread.sleep(1000) val reportIds = setOf(rId, raId, riId, iarId) @@ -841,7 +839,7 @@ class QueryNotificationConfigIT : PluginRestTestCase() { "this is a sample config name", "this is a sample config description", ConfigType.CHIME, - EnumSet.of(ALERTING, REPORTS), + setOf(FEATURE_ALERTING, FEATURE_REPORTS), isEnabled = true, configData = sampleChime ) diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/SlackNotificationConfigCrudIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/SlackNotificationConfigCrudIT.kt index 32822dea..56117cd6 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/SlackNotificationConfigCrudIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/SlackNotificationConfigCrudIT.kt @@ -28,8 +28,9 @@ package org.opensearch.integtest.config import org.junit.Assert +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_INDEX_MANAGEMENT +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_REPORTS import org.opensearch.commons.notifications.model.ConfigType -import org.opensearch.commons.notifications.model.Feature import org.opensearch.commons.notifications.model.NotificationConfig import org.opensearch.commons.notifications.model.Slack import org.opensearch.integtest.PluginRestTestCase @@ -37,7 +38,6 @@ import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI import org.opensearch.notifications.verifySingleConfigEquals import org.opensearch.rest.RestRequest import org.opensearch.rest.RestStatus -import java.util.EnumSet class SlackNotificationConfigCrudIT : PluginRestTestCase() { @@ -48,7 +48,7 @@ class SlackNotificationConfigCrudIT : PluginRestTestCase() { "this is a sample config name", "this is a sample config description", ConfigType.SLACK, - EnumSet.of(Feature.INDEX_MANAGEMENT, Feature.REPORTS), + setOf(FEATURE_INDEX_MANAGEMENT, FEATURE_REPORTS), isEnabled = true, configData = sampleSlack ) @@ -79,7 +79,7 @@ class SlackNotificationConfigCrudIT : PluginRestTestCase() { Assert.assertNotNull(configId) Thread.sleep(1000) - // Get slack notification config + // Get Slack notification config val getConfigResponse = executeRequest( RestRequest.Method.GET.name, @@ -107,7 +107,7 @@ class SlackNotificationConfigCrudIT : PluginRestTestCase() { "this is a updated config name", "this is a updated config description", ConfigType.SLACK, - EnumSet.of(Feature.INDEX_MANAGEMENT, Feature.REPORTS), + setOf(FEATURE_INDEX_MANAGEMENT, FEATURE_REPORTS), isEnabled = true, configData = updatedSlack ) @@ -137,7 +137,7 @@ class SlackNotificationConfigCrudIT : PluginRestTestCase() { Assert.assertEquals(configId, updateResponse.get("config_id").asString) Thread.sleep(1000) - // Get updated slack notification config + // Get updated Slack notification config val getUpdatedConfigResponse = executeRequest( RestRequest.Method.GET.name, @@ -176,7 +176,7 @@ class SlackNotificationConfigCrudIT : PluginRestTestCase() { "this is a sample config name", "this is a sample config description", ConfigType.SLACK, - EnumSet.of(Feature.INDEX_MANAGEMENT, Feature.REPORTS), + setOf(FEATURE_INDEX_MANAGEMENT, FEATURE_REPORTS), isEnabled = true, configData = sampleSlack ) diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/WebhookNotificationConfigCrudIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/WebhookNotificationConfigCrudIT.kt index 5f8c022d..b7ad33e3 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/WebhookNotificationConfigCrudIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/WebhookNotificationConfigCrudIT.kt @@ -28,8 +28,10 @@ package org.opensearch.integtest.config import org.junit.Assert +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_ALERTING +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_INDEX_MANAGEMENT +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_REPORTS import org.opensearch.commons.notifications.model.ConfigType -import org.opensearch.commons.notifications.model.Feature import org.opensearch.commons.notifications.model.NotificationConfig import org.opensearch.commons.notifications.model.Webhook import org.opensearch.integtest.PluginRestTestCase @@ -37,7 +39,6 @@ import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI import org.opensearch.notifications.verifySingleConfigEquals import org.opensearch.rest.RestRequest import org.opensearch.rest.RestStatus -import java.util.EnumSet class WebhookNotificationConfigCrudIT : PluginRestTestCase() { @@ -51,7 +52,7 @@ class WebhookNotificationConfigCrudIT : PluginRestTestCase() { "this is a sample config name", "this is a sample config description", ConfigType.WEBHOOK, - EnumSet.of(Feature.INDEX_MANAGEMENT, Feature.REPORTS, Feature.ALERTING), + setOf(FEATURE_INDEX_MANAGEMENT, FEATURE_REPORTS, FEATURE_ALERTING), isEnabled = true, configData = sampleWebhook ) @@ -116,7 +117,7 @@ class WebhookNotificationConfigCrudIT : PluginRestTestCase() { "this is a updated config name", "this is a updated config description", ConfigType.WEBHOOK, - EnumSet.of(Feature.INDEX_MANAGEMENT, Feature.REPORTS), + setOf(FEATURE_INDEX_MANAGEMENT, FEATURE_REPORTS), isEnabled = true, configData = updatedWebhook ) @@ -185,7 +186,7 @@ class WebhookNotificationConfigCrudIT : PluginRestTestCase() { "this is a sample config name", "this is a sample config description", ConfigType.WEBHOOK, - EnumSet.of(Feature.INDEX_MANAGEMENT, Feature.REPORTS, Feature.ALERTING), + setOf(FEATURE_INDEX_MANAGEMENT, FEATURE_REPORTS, FEATURE_ALERTING), isEnabled = true, configData = sampleWebhook ) diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/features/GetNotificationFeatureChannelListIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/features/GetNotificationFeatureChannelListIT.kt index 78cebeac..b800a602 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/features/GetNotificationFeatureChannelListIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/features/GetNotificationFeatureChannelListIT.kt @@ -13,8 +13,10 @@ package org.opensearch.integtest.features import com.google.gson.JsonObject import org.junit.Assert +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_ALERTING +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_INDEX_MANAGEMENT +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_REPORTS import org.opensearch.commons.notifications.model.ConfigType -import org.opensearch.commons.notifications.model.Feature import org.opensearch.integtest.PluginRestTestCase import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI import org.opensearch.rest.RestRequest @@ -28,7 +30,7 @@ class GetNotificationFeatureChannelListIT : PluginRestTestCase() { nameSubstring: String, configType: ConfigType, isEnabled: Boolean, - features: Set, + features: Set, smtpAccountId: String = "", emailGroupId: Set = setOf() ): String { @@ -91,7 +93,7 @@ class GetNotificationFeatureChannelListIT : PluginRestTestCase() { nameSubstring: String = "", configType: ConfigType = ConfigType.SLACK, isEnabled: Boolean = true, - features: Set = setOf(Feature.ALERTING, Feature.INDEX_MANAGEMENT, Feature.REPORTS), + features: Set = setOf(FEATURE_ALERTING, FEATURE_INDEX_MANAGEMENT, FEATURE_REPORTS), smtpAccountId: String = "", emailGroupId: Set = setOf() ): String { @@ -156,15 +158,6 @@ class GetNotificationFeatureChannelListIT : PluginRestTestCase() { ) } - fun `test Get feature channel list should error for invalid feature`() { - executeRequest( - RestRequest.Method.GET.name, - "$PLUGIN_BASE_URI/feature/channels/new_feature", - "", - RestStatus.BAD_REQUEST.status - ) - } - fun `test getFeatureChannelList should return only channels`() { val slackId = createConfig(configType = ConfigType.SLACK) val chimeId = createConfig(configType = ConfigType.CHIME) @@ -191,10 +184,10 @@ class GetNotificationFeatureChannelListIT : PluginRestTestCase() { } fun `test getFeatureChannelList should return only channels corresponding to feature`() { - val alertingOnlyIds: Set = (1..5).map { createConfig(features = setOf(Feature.ALERTING)) }.toSet() - val reportsOnlyIds: Set = (1..5).map { createConfig(features = setOf(Feature.REPORTS)) }.toSet() + val alertingOnlyIds: Set = (1..5).map { createConfig(features = setOf(FEATURE_ALERTING)) }.toSet() + val reportsOnlyIds: Set = (1..5).map { createConfig(features = setOf(FEATURE_REPORTS)) }.toSet() val ismAndAlertingIds: Set = (1..5).map { - createConfig(features = setOf(Feature.ALERTING, Feature.INDEX_MANAGEMENT)) + createConfig(features = setOf(FEATURE_ALERTING, FEATURE_INDEX_MANAGEMENT)) }.toSet() Thread.sleep(1000) val alertingIds = alertingOnlyIds.union(ismAndAlertingIds) diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/ObjectEqualsHelpers.kt b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/ObjectEqualsHelpers.kt index 8c7a73e9..f0c06b62 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/ObjectEqualsHelpers.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/ObjectEqualsHelpers.kt @@ -33,7 +33,6 @@ import org.opensearch.commons.notifications.model.Chime import org.opensearch.commons.notifications.model.ConfigType import org.opensearch.commons.notifications.model.Email import org.opensearch.commons.notifications.model.EmailGroup -import org.opensearch.commons.notifications.model.Feature import org.opensearch.commons.notifications.model.MethodType import org.opensearch.commons.notifications.model.NotificationConfig import org.opensearch.commons.notifications.model.Slack @@ -82,7 +81,7 @@ fun verifyEquals(config: NotificationConfig, jsonObject: JsonObject) { Assert.assertEquals(config.isEnabled, jsonObject.get("is_enabled").asBoolean) val features = jsonObject.get("feature_list").asJsonArray Assert.assertEquals(config.features.size, features.size()) - features.forEach { config.features.contains(Feature.fromTagOrDefault(it.asString)) } + features.forEach { config.features.contains(it.asString) } when (config.configType) { ConfigType.SLACK -> verifyEquals((config.configData as Slack), jsonObject.get("slack").asJsonObject) ConfigType.CHIME -> verifyEquals((config.configData as Chime), jsonObject.get("chime").asJsonObject) diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/model/NotificationConfigDocTests.kt b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/model/NotificationConfigDocTests.kt index b224c4c0..4e50e97d 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/model/NotificationConfigDocTests.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/model/NotificationConfigDocTests.kt @@ -29,14 +29,13 @@ package org.opensearch.notifications.model import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_REPORTS import org.opensearch.commons.notifications.model.ConfigType -import org.opensearch.commons.notifications.model.Feature import org.opensearch.commons.notifications.model.NotificationConfig import org.opensearch.commons.notifications.model.Slack import org.opensearch.notifications.createObjectFromJsonString import org.opensearch.notifications.getJsonString import java.time.Instant -import java.util.EnumSet internal class NotificationConfigDocTests { @@ -55,7 +54,7 @@ internal class NotificationConfigDocTests { "name", "description", ConfigType.SLACK, - EnumSet.of(Feature.REPORTS), + setOf(FEATURE_REPORTS), configData = sampleSlack ) val configDoc = NotificationConfigDoc(metadata, config) @@ -79,7 +78,7 @@ internal class NotificationConfigDocTests { "name", "description", ConfigType.SLACK, - EnumSet.of(Feature.REPORTS), + setOf(FEATURE_REPORTS), configData = sampleSlack ) val configDoc = NotificationConfigDoc(metadata, config) diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/model/NotificationEventDocTests.kt b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/model/NotificationEventDocTests.kt index 788fea99..e7a82082 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/model/NotificationEventDocTests.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/model/NotificationEventDocTests.kt @@ -29,11 +29,11 @@ package org.opensearch.notifications.model import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_ALERTING import org.opensearch.commons.notifications.model.ConfigType import org.opensearch.commons.notifications.model.DeliveryStatus import org.opensearch.commons.notifications.model.EventSource import org.opensearch.commons.notifications.model.EventStatus -import org.opensearch.commons.notifications.model.Feature import org.opensearch.commons.notifications.model.NotificationEvent import org.opensearch.commons.notifications.model.SeverityType import org.opensearch.notifications.createObjectFromJsonString @@ -55,7 +55,7 @@ internal class NotificationEventDocTests { val sampleEventSource = EventSource( "title", "reference_id", - Feature.ALERTING, + FEATURE_ALERTING, tags = listOf("tag1", "tag2"), severity = SeverityType.INFO ) @@ -85,7 +85,7 @@ internal class NotificationEventDocTests { val eventSource = EventSource( "title", "reference_id", - Feature.ALERTING, + FEATURE_ALERTING, tags = listOf("tag1", "tag2"), severity = SeverityType.INFO ) From 09e6e37b85af4fcbbbea85dca1256778c56b5901 Mon Sep 17 00:00:00 2001 From: Drew Baugher <46505179+dbbaughe@users.noreply.github.com> Date: Fri, 13 Aug 2021 13:55:29 -0700 Subject: [PATCH 17/29] Adds PublishNotificationAction for legacy ISM implementation (#254) * Adds PublishNotificationAction for legacy ISM implementation Signed-off-by: Drew Baugher <46505179+dbbaughe@users.noreply.github.com> * Fixes rename of feature and required referenceId from merged changes Signed-off-by: Drew Baugher <46505179+dbbaughe@users.noreply.github.com> --- .../notifications/NotificationPlugin.kt | 5 ++ .../action/PublishNotificationAction.kt | 72 +++++++++++++++++++ .../send/SendMessageActionHelper.kt | 65 +++++++++++++++++ .../notifications/action/PluginActionTests.kt | 20 ++++++ 4 files changed, 162 insertions(+) create mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/PublishNotificationAction.kt diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationPlugin.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationPlugin.kt index a964c68e..9dc54a93 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationPlugin.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationPlugin.kt @@ -50,6 +50,7 @@ import org.opensearch.notifications.action.GetFeatureChannelListAction import org.opensearch.notifications.action.GetNotificationConfigAction import org.opensearch.notifications.action.GetNotificationEventAction import org.opensearch.notifications.action.GetPluginFeaturesAction +import org.opensearch.notifications.action.PublishNotificationAction import org.opensearch.notifications.action.SendNotificationAction import org.opensearch.notifications.action.UpdateNotificationConfigAction import org.opensearch.notifications.index.ConfigIndexingActions @@ -162,6 +163,10 @@ internal class NotificationPlugin : ActionPlugin, Plugin() { ActionPlugin.ActionHandler( NotificationsActions.SEND_NOTIFICATION_ACTION_TYPE, SendNotificationAction::class.java + ), + ActionPlugin.ActionHandler( + NotificationsActions.LEGACY_PUBLISH_NOTIFICATION_ACTION_TYPE, + PublishNotificationAction::class.java ) ) } diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/PublishNotificationAction.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/PublishNotificationAction.kt new file mode 100644 index 00000000..7d090405 --- /dev/null +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/PublishNotificationAction.kt @@ -0,0 +1,72 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.action + +import org.opensearch.action.ActionListener +import org.opensearch.action.ActionRequest +import org.opensearch.action.support.ActionFilters +import org.opensearch.client.Client +import org.opensearch.common.inject.Inject +import org.opensearch.common.xcontent.NamedXContentRegistry +import org.opensearch.commons.authuser.User +import org.opensearch.commons.notifications.action.LegacyPublishNotificationRequest +import org.opensearch.commons.notifications.action.LegacyPublishNotificationResponse +import org.opensearch.commons.notifications.action.NotificationsActions +import org.opensearch.commons.utils.recreateObject +import org.opensearch.notifications.send.SendMessageActionHelper +import org.opensearch.tasks.Task +import org.opensearch.transport.TransportService + +/** + * Publish Notification transport action + * + * This action is intended only for Index Management use case to support + * the legacy embedded destinations that are on its policies. No other plugin + * should utilize this action. + */ +internal class PublishNotificationAction @Inject constructor( + transportService: TransportService, + client: Client, + actionFilters: ActionFilters, + val xContentRegistry: NamedXContentRegistry +) : PluginBaseAction( + NotificationsActions.LEGACY_PUBLISH_NOTIFICATION_NAME, + transportService, + client, + actionFilters, + ::LegacyPublishNotificationRequest +) { + + /** + * {@inheritDoc} + * Transform the request and call super.doExecute() to support call from other plugins. + */ + override fun doExecute( + task: Task?, + request: ActionRequest, + listener: ActionListener + ) { + val transformedRequest = request as? LegacyPublishNotificationRequest + ?: recreateObject(request) { LegacyPublishNotificationRequest(it) } + super.doExecute(task, transformedRequest, listener) + } + + /** + * {@inheritDoc} + */ + override fun executeRequest( + request: LegacyPublishNotificationRequest, + user: User? + ): LegacyPublishNotificationResponse { + return SendMessageActionHelper.executeLegacyRequest(request) + } +} 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 f4ecd5e1..21ea7e68 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 @@ -16,6 +16,13 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking import org.opensearch.OpenSearchStatusException import org.opensearch.commons.authuser.User +import org.opensearch.commons.destination.message.LegacyBaseMessage +import org.opensearch.commons.destination.message.LegacyCustomWebhookMessage +import org.opensearch.commons.destination.message.LegacyDestinationType +import org.opensearch.commons.destination.response.LegacyDestinationResponse +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_INDEX_MANAGEMENT +import org.opensearch.commons.notifications.action.LegacyPublishNotificationRequest +import org.opensearch.commons.notifications.action.LegacyPublishNotificationResponse import org.opensearch.commons.notifications.action.SendNotificationRequest import org.opensearch.commons.notifications.action.SendNotificationResponse import org.opensearch.commons.notifications.model.ChannelMessage @@ -98,6 +105,19 @@ object SendMessageActionHelper { return SendNotificationResponse(docId) } + /** + * Send legacy notification message intended only for Index Management plugin. + * @param request request object + */ + fun executeLegacyRequest(request: LegacyPublishNotificationRequest): LegacyPublishNotificationResponse { + val baseMessage = request.baseMessage + val response: LegacyDestinationResponse + runBlocking { + response = sendMessageToLegacyDestination(baseMessage) + } + return LegacyPublishNotificationResponse(response) + } + /** * Create message content from the request parameters * @param eventSource event source of request @@ -204,6 +224,51 @@ object SendMessageActionHelper { } } + /** + * Send message to a legacy destination intended only for Index Management + * + * Currently this simply converts the legacy base message to the equivalent destination classes that exist + * for the notification channels and utilizes the [sendMessageThroughSpi] method. If we get to the point + * where this method seems to be holding back notification channels from adding new functionality we can + * refactor this to have it's own internal private spi call to completely decouple them instead. + * + * @param baseMessage legacy base message + * @return notification delivery status for the legacy destination + */ + private fun sendMessageToLegacyDestination(baseMessage: LegacyBaseMessage): LegacyDestinationResponse { + val message = MessageContent(title = "Index Management Notification", textDescription = baseMessage.messageContent) + // These legacy destination calls do not have reference Ids, just passing index management feature constant + return when (baseMessage.channelType) { + LegacyDestinationType.LEGACY_SLACK -> { + val destination = SlackDestination(baseMessage.url) + val status = sendMessageThroughSpi(destination, message, FEATURE_INDEX_MANAGEMENT) + LegacyDestinationResponse.Builder().withStatusCode(status.statusCode) + .withResponseContent(status.statusText).build() + } + LegacyDestinationType.LEGACY_CHIME -> { + val destination = ChimeDestination(baseMessage.url) + val status = sendMessageThroughSpi(destination, message, FEATURE_INDEX_MANAGEMENT) + LegacyDestinationResponse.Builder().withStatusCode(status.statusCode) + .withResponseContent(status.statusText).build() + } + LegacyDestinationType.LEGACY_CUSTOM_WEBHOOK -> { + val destination = CustomWebhookDestination( + (baseMessage as LegacyCustomWebhookMessage).uri.toString(), + baseMessage.headerParams, + baseMessage.method + ) + val status = sendMessageThroughSpi(destination, message, FEATURE_INDEX_MANAGEMENT) + 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) + .withResponseContent("No channel type given (null) for publishing to legacy destination").build() + } + } + } + /** * Check if channel is eligible to send message, return error status if not * @param eventSource event source information diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/action/PluginActionTests.kt b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/action/PluginActionTests.kt index 1407d07d..b875526c 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/action/PluginActionTests.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/action/PluginActionTests.kt @@ -23,6 +23,7 @@ import org.opensearch.action.ActionListener import org.opensearch.action.support.ActionFilters import org.opensearch.client.Client import org.opensearch.common.xcontent.NamedXContentRegistry +import org.opensearch.commons.destination.response.LegacyDestinationResponse import org.opensearch.commons.notifications.action.BaseResponse import org.opensearch.commons.notifications.action.CreateNotificationConfigRequest import org.opensearch.commons.notifications.action.CreateNotificationConfigResponse @@ -36,6 +37,8 @@ import org.opensearch.commons.notifications.action.GetNotificationEventRequest import org.opensearch.commons.notifications.action.GetNotificationEventResponse import org.opensearch.commons.notifications.action.GetPluginFeaturesRequest import org.opensearch.commons.notifications.action.GetPluginFeaturesResponse +import org.opensearch.commons.notifications.action.LegacyPublishNotificationRequest +import org.opensearch.commons.notifications.action.LegacyPublishNotificationResponse import org.opensearch.commons.notifications.action.SendNotificationRequest import org.opensearch.commons.notifications.action.SendNotificationResponse import org.opensearch.commons.notifications.action.UpdateNotificationConfigRequest @@ -201,6 +204,23 @@ internal class PluginActionTests { sendNotificationAction.execute(task, request, AssertionListener(response)) } + @Test + fun `Publish notification action should call back action listener`() { + val request = mock(LegacyPublishNotificationRequest::class.java) + val response = LegacyPublishNotificationResponse( + LegacyDestinationResponse.Builder().withStatusCode(200).withResponseContent("Hello world").build() + ) + + // Mock singleton's method by mockk framework + mockkObject(SendMessageActionHelper) + every { SendMessageActionHelper.executeLegacyRequest(request) } returns response + + val publishNotificationAction = PublishNotificationAction( + transportService, client, actionFilters, xContentRegistry + ) + publishNotificationAction.execute(task, request, AssertionListener(response)) + } + /** * This listener class is to assert on response rather than verify it called. * The reason why this is required is because it is harder to do the latter From b109cdb86c08bafdd55dfb2f26722bd8176e5fd4 Mon Sep 17 00:00:00 2001 From: Anantha Krishna Bhatta Date: Mon, 16 Aug 2021 13:08:40 -0700 Subject: [PATCH 18/29] Add support for sending email over SES in notification plugin [Tests] Added intergration tests for config operations Signed-off-by: @akbhatta --- .../index/ConfigIndexingActions.kt | 10 +- .../notifications/index/ConfigQueryHelper.kt | 9 +- .../NotificationConfigRestHandler.kt | 3 + .../send/SendMessageActionHelper.kt | 63 +++- .../notifications-config-mapping.yml | 15 + .../config/EmailNotificationConfigCrudIT.kt | 278 +++++++++++++++++- .../notifications/ObjectEqualsHelpers.kt | 18 ++ .../spi/model/destination/SesDestination.kt | 1 + 8 files changed, 381 insertions(+), 16 deletions(-) diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt index 1c3efee9..ec710148 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt @@ -129,6 +129,13 @@ object ConfigIndexingActions { RestStatus.NOT_ACCEPTABLE ) } + ConfigType.SES_ACCOUNT -> if (it.docInfo.id != email.emailAccountID) { + // Email Account ID is specified as Email Group ID + throw OpenSearchStatusException( + "configId ${it.docInfo.id} is not a valid email group ID", + RestStatus.NOT_ACCEPTABLE + ) + } else -> { // Config ID is neither Email Group ID or valid Email Account ID throw OpenSearchStatusException( @@ -386,7 +393,8 @@ object ConfigIndexingActions { ConfigType.SLACK.tag, ConfigType.CHIME.tag, ConfigType.WEBHOOK.tag, - ConfigType.EMAIL.tag + ConfigType.EMAIL.tag, + ConfigType.SNS.tag ) } diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigQueryHelper.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigQueryHelper.kt index 97f8108c..b0992ba9 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigQueryHelper.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigQueryHelper.kt @@ -41,6 +41,7 @@ import org.opensearch.commons.notifications.NotificationConstants.METHOD_TAG import org.opensearch.commons.notifications.NotificationConstants.NAME_TAG import org.opensearch.commons.notifications.NotificationConstants.QUERY_TAG import org.opensearch.commons.notifications.NotificationConstants.RECIPIENT_LIST_TAG +import org.opensearch.commons.notifications.NotificationConstants.REGION_TAG import org.opensearch.commons.notifications.NotificationConstants.ROLE_ARN_TAG import org.opensearch.commons.notifications.NotificationConstants.TOPIC_ARN_TAG import org.opensearch.commons.notifications.NotificationConstants.UPDATED_TIME_TAG @@ -48,6 +49,7 @@ import org.opensearch.commons.notifications.NotificationConstants.URL_TAG import org.opensearch.commons.notifications.model.ConfigType.CHIME import org.opensearch.commons.notifications.model.ConfigType.EMAIL import org.opensearch.commons.notifications.model.ConfigType.EMAIL_GROUP +import org.opensearch.commons.notifications.model.ConfigType.SES_ACCOUNT import org.opensearch.commons.notifications.model.ConfigType.SLACK import org.opensearch.commons.notifications.model.ConfigType.SMTP_ACCOUNT import org.opensearch.commons.notifications.model.ConfigType.SNS @@ -76,7 +78,8 @@ object ConfigQueryHelper { FEATURE_LIST_TAG, "${EMAIL.tag}.$EMAIL_ACCOUNT_ID_TAG", "${EMAIL.tag}.$EMAIL_GROUP_ID_LIST_TAG", - "${SMTP_ACCOUNT.tag}.$METHOD_TAG" + "${SMTP_ACCOUNT.tag}.$METHOD_TAG", + "${SES_ACCOUNT.tag}.$REGION_TAG" ) private val TEXT_FIELDS = setOf( NAME_TAG, @@ -89,7 +92,9 @@ object ConfigQueryHelper { "${SMTP_ACCOUNT.tag}.$FROM_ADDRESS_TAG", "${EMAIL_GROUP.tag}.$RECIPIENT_LIST_TAG", "${SNS.tag}.$TOPIC_ARN_TAG", - "${SNS.tag}.$ROLE_ARN_TAG" + "${SNS.tag}.$ROLE_ARN_TAG", + "${SES_ACCOUNT.tag}.$ROLE_ARN_TAG", + "${SES_ACCOUNT.tag}.$FROM_ADDRESS_TAG" ) private val METADATA_FIELDS = METADATA_RANGE_FIELDS diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationConfigRestHandler.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationConfigRestHandler.kt index 5a8ac03b..2475e0bb 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationConfigRestHandler.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationConfigRestHandler.kt @@ -145,6 +145,9 @@ internal class NotificationConfigRestHandler : PluginBaseHandler() { * smtp_account.recipient_list=abc,xyz * sns.topic_arn=abc,xyz * sns.role_arn=abc,xyz + * ses_account.region=abc,xyz + * ses_account.role_arn=abc,xyz + * ses_account.from_address=abc,xyz * query=search all above fields * Request body: Ref [org.opensearch.commons.notifications.action.GetNotificationConfigRequest] * Response body: [org.opensearch.commons.notifications.action.GetNotificationConfigResponse] 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 21ea7e68..e34868bc 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 @@ -35,6 +35,7 @@ import org.opensearch.commons.notifications.model.EmailRecipientStatus import org.opensearch.commons.notifications.model.EventSource import org.opensearch.commons.notifications.model.EventStatus import org.opensearch.commons.notifications.model.NotificationEvent +import org.opensearch.commons.notifications.model.SesAccount import org.opensearch.commons.notifications.model.Slack import org.opensearch.commons.notifications.model.SmtpAccount import org.opensearch.commons.notifications.model.Sns @@ -53,6 +54,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.SesDestination import org.opensearch.notifications.spi.model.destination.SlackDestination import org.opensearch.notifications.spi.model.destination.SmtpDestination import org.opensearch.notifications.spi.model.destination.SnsDestination @@ -211,7 +213,7 @@ object SendMessageActionHelper { eventStatus, eventSource.referenceId ) - ConfigType.SES_ACCOUNT -> null // TODO : Implement + ConfigType.SES_ACCOUNT -> null ConfigType.SMTP_ACCOUNT -> null ConfigType.EMAIL_GROUP -> null ConfigType.SNS -> sendSNSMessage(configData as Sns, message, eventStatus, eventSource.referenceId) @@ -236,7 +238,8 @@ object SendMessageActionHelper { * @return notification delivery status for the legacy destination */ private fun sendMessageToLegacyDestination(baseMessage: LegacyBaseMessage): LegacyDestinationResponse { - val message = MessageContent(title = "Index Management Notification", textDescription = baseMessage.messageContent) + val message = + MessageContent(title = "Index Management Notification", textDescription = baseMessage.messageContent) // These legacy destination calls do not have reference Ids, just passing index management feature constant return when (baseMessage.channelType) { LegacyDestinationType.LEGACY_SLACK -> { @@ -340,22 +343,35 @@ object SendMessageActionHelper { eventStatus: EventStatus, referenceId: String ): EventStatus { - val smtpAccountDocInfo = childConfigs.find { it.docInfo.id == email.emailAccountID } + val accountDocInfo = childConfigs.find { it.docInfo.id == email.emailAccountID } val groups = childConfigs.filter { email.emailGroupIds.contains(it.docInfo.id) } val groupRecipients = groups.map { (it.configDoc.config.configData as EmailGroup).recipients }.flatten() val recipients = email.recipients.union(groupRecipients) val emailRecipientStatus: List - val smtpAccountConfig = smtpAccountDocInfo?.configDoc!!.config + val accountConfig = accountDocInfo?.configDoc!!.config runBlocking { val statusDeferredList = recipients.map { async(Dispatchers.IO) { - sendEmailFromSmtpAccount( - smtpAccountConfig.name, - smtpAccountConfig.configData as SmtpAccount, - it, - message, - referenceId - ) + when (accountConfig.configType) { + ConfigType.SMTP_ACCOUNT -> sendEmailFromSmtpAccount( + accountConfig.name, + accountConfig.configData as SmtpAccount, + it, + message, + referenceId + ) + ConfigType.SES_ACCOUNT -> sendEmailFromSesAccount( + accountConfig.name, + accountConfig.configData as SesAccount, + it, + message, + referenceId + ) + else -> EmailRecipientStatus( + it, + DeliveryStatus(RestStatus.NOT_ACCEPTABLE.name, "email account type not enabled") + ) + } } } emailRecipientStatus = statusDeferredList.awaitAll() @@ -380,7 +396,6 @@ object SendMessageActionHelper { /** * send message to smtp destination */ - @Suppress("UnusedPrivateMember") private fun sendEmailFromSmtpAccount( accountName: String, smtpAccount: SmtpAccount, @@ -403,6 +418,30 @@ object SendMessageActionHelper { ) } + /** + * send message to ses destination + */ + private fun sendEmailFromSesAccount( + accountName: String, + sesAccount: SesAccount, + recipient: String, + message: MessageContent, + referenceId: String + ): EmailRecipientStatus { + val destination = SesDestination( + accountName, + sesAccount.awsRegion, + sesAccount.roleArn, + sesAccount.fromAddress, + recipient + ) + val status = sendMessageThroughSpi(destination, message, referenceId) + return EmailRecipientStatus( + recipient, + DeliveryStatus(status.statusCode.toString(), status.statusText) + ) + } + /** * send message to SNS destination */ diff --git a/notifications/notifications/src/main/resources/notifications-config-mapping.yml b/notifications/notifications/src/main/resources/notifications-config-mapping.yml index d2ff05a2..26eb0b68 100644 --- a/notifications/notifications/src/main/resources/notifications-config-mapping.yml +++ b/notifications/notifications/src/main/resources/notifications-config-mapping.yml @@ -130,6 +130,21 @@ properties: fields: keyword: type: keyword + ses_account: # smtp account configuration + type: object + properties: + region: + type: keyword + role_arn: + type: text + fields: + keyword: + type: keyword + from_address: + type: text + fields: + keyword: + type: keyword email_group: # email group configuration type: object properties: diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/EmailNotificationConfigCrudIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/EmailNotificationConfigCrudIT.kt index 5e249a34..5f754ae5 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/EmailNotificationConfigCrudIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/EmailNotificationConfigCrudIT.kt @@ -35,6 +35,7 @@ import org.opensearch.commons.notifications.model.Email import org.opensearch.commons.notifications.model.EmailGroup import org.opensearch.commons.notifications.model.MethodType import org.opensearch.commons.notifications.model.NotificationConfig +import org.opensearch.commons.notifications.model.SesAccount import org.opensearch.commons.notifications.model.SmtpAccount import org.opensearch.integtest.PluginRestTestCase import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI @@ -47,7 +48,7 @@ import org.opensearch.rest.RestStatus class EmailNotificationConfigCrudIT : PluginRestTestCase() { - fun `test Create, Get, Update, Delete email notification config using REST client`() { + fun `test Create, Get, Update, Delete smtp email notification config using REST client`() { // Create sample smtp account config request reference val sampleSmtpAccount = SmtpAccount( "smtp.domain.com", @@ -326,6 +327,281 @@ class EmailNotificationConfigCrudIT : PluginRestTestCase() { Thread.sleep(100) } + fun `test Create, Get, Update, Delete ses email notification config using REST client`() { + // Create sample ses account config request reference + val sampleSesAccount = SesAccount( + "us-east-1", + "arn:aws:iam::012345678912:role/iam-test", + "from@domain.com" + ) + val sesAccountConfig = NotificationConfig( + "this is a sample ses account config name", + "this is a sample ses account config description", + ConfigType.SES_ACCOUNT, + setOf(FEATURE_REPORTS), + isEnabled = true, + configData = sampleSesAccount + ) + + // Create ses account notification config + val createSesAccountRequestJsonString = """ + { + "config":{ + "name":"${sesAccountConfig.name}", + "description":"${sesAccountConfig.description}", + "config_type":"ses_account", + "feature_list":[ + "${sesAccountConfig.features.elementAt(0)}" + ], + "is_enabled":${sesAccountConfig.isEnabled}, + "ses_account":{ + "region":"${sampleSesAccount.awsRegion}", + "role_arn":"${sampleSesAccount.roleArn}", + "from_address":"${sampleSesAccount.fromAddress}" + } + } + } + """.trimIndent() + val createSesAccountResponse = executeRequest( + RestRequest.Method.POST.name, + "$PLUGIN_BASE_URI/configs", + createSesAccountRequestJsonString, + RestStatus.OK.status + ) + val sesAccountConfigId = createSesAccountResponse.get("config_id").asString + Assert.assertNotNull(sesAccountConfigId) + Thread.sleep(100) + + // Create sample email group config request reference + val sampleEmailGroup = EmailGroup(listOf("email1@email.com", "email2@email.com")) + val emailGroupConfig = NotificationConfig( + "this is a sample email group config name", + "this is a sample email group config description", + ConfigType.EMAIL_GROUP, + setOf(FEATURE_REPORTS), + isEnabled = true, + configData = sampleEmailGroup + ) + + // Create email group notification config + val createEmailGroupRequestJsonString = """ + { + "config":{ + "name":"${emailGroupConfig.name}", + "description":"${emailGroupConfig.description}", + "config_type":"email_group", + "feature_list":[ + "${emailGroupConfig.features.elementAt(0)}" + ], + "is_enabled":${emailGroupConfig.isEnabled}, + "email_group":{ + "recipient_list":[ + "${sampleEmailGroup.recipients[0]}", + "${sampleEmailGroup.recipients[1]}" + ] + } + } + } + """.trimIndent() + val createEmailGroupResponse = executeRequest( + RestRequest.Method.POST.name, + "$PLUGIN_BASE_URI/configs", + createEmailGroupRequestJsonString, + RestStatus.OK.status + ) + val emailGroupConfigId = createEmailGroupResponse.get("config_id").asString + Assert.assertNotNull(emailGroupConfigId) + Thread.sleep(100) + + // Create sample email config request reference + val sampleEmail = Email( + sesAccountConfigId, + listOf("default-email1@email.com", "default-email2@email.com"), + listOf(emailGroupConfigId) + ) + val emailConfig = NotificationConfig( + "this is a sample config name", + "this is a sample config description", + ConfigType.EMAIL, + setOf(FEATURE_REPORTS), + isEnabled = true, + configData = sampleEmail + ) + + // Create email notification config + val createEmailRequestJsonString = """ + { + "config":{ + "name":"${emailConfig.name}", + "description":"${emailConfig.description}", + "config_type":"email", + "feature_list":[ + "${emailConfig.features.elementAt(0)}" + ], + "is_enabled":${emailConfig.isEnabled}, + "email":{ + "email_account_id":"${sampleEmail.emailAccountID}", + "recipient_list":[ + "${sampleEmail.recipients[0]}", + "${sampleEmail.recipients[1]}" + ], + "email_group_id_list":[ + "${sampleEmail.emailGroupIds[0]}" + ] + } + } + } + """.trimIndent() + val createEmailResponse = executeRequest( + RestRequest.Method.POST.name, + "$PLUGIN_BASE_URI/configs", + createEmailRequestJsonString, + RestStatus.OK.status + ) + val emailConfigId = createEmailResponse.get("config_id").asString + Assert.assertNotNull(emailConfigId) + Thread.sleep(1000) + + // Get email notification config + val getSesAccountResponse = executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/configs/$sesAccountConfigId", + "", + RestStatus.OK.status + ) + verifySingleConfigEquals(sesAccountConfigId, sesAccountConfig, getSesAccountResponse) + Thread.sleep(100) + + val getEmailGroupResponse = executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/configs/$emailGroupConfigId", + "", + RestStatus.OK.status + ) + verifySingleConfigEquals(emailGroupConfigId, emailGroupConfig, getEmailGroupResponse) + Thread.sleep(100) + + val getEmailResponse = executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/configs/$emailConfigId", + "", + RestStatus.OK.status + ) + verifySingleConfigEquals(emailConfigId, emailConfig, getEmailResponse) + Thread.sleep(100) + + // Get all notification config + + val getAllConfigResponse = executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/configs", + "", + RestStatus.OK.status + ) + verifyMultiConfigEquals( + mapOf( + Pair(sesAccountConfigId, sesAccountConfig), + Pair(emailGroupConfigId, emailGroupConfig), + Pair(emailConfigId, emailConfig) + ), + getAllConfigResponse + ) + Thread.sleep(100) + + // Updated ses account config object + val updatedSesAccount = SesAccount( + "us-west-2", + "arn:aws:iam::012345678912:role/updated-role-test", + "updated-from@domain.com" + ) + val updatedSesAccountConfig = NotificationConfig( + "this is a updated ses account config name", + "this is a updated ses account config description", + ConfigType.SES_ACCOUNT, + setOf(FEATURE_REPORTS), + isEnabled = true, + configData = updatedSesAccount + ) + + // Update ses account notification config + val updateSesAccountRequestJsonString = """ + { + "config":{ + "name":"${updatedSesAccountConfig.name}", + "description":"${updatedSesAccountConfig.description}", + "config_type":"ses_account", + "feature_list":[ + "${updatedSesAccountConfig.features.elementAt(0)}" + ], + "is_enabled":${updatedSesAccountConfig.isEnabled}, + "ses_account":{ + "region":"${updatedSesAccount.awsRegion}", + "role_arn":"${updatedSesAccount.roleArn}", + "from_address":"${updatedSesAccount.fromAddress}" + } + } + } + """.trimIndent() + + val updateSesAccountResponse = executeRequest( + RestRequest.Method.PUT.name, + "$PLUGIN_BASE_URI/configs/$sesAccountConfigId", + updateSesAccountRequestJsonString, + RestStatus.OK.status + ) + Assert.assertEquals(sesAccountConfigId, updateSesAccountResponse.get("config_id").asString) + + Thread.sleep(1000) + + // Get updated ses account config + + val getUpdatedSesAccountResponse = executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/configs/$sesAccountConfigId", + "", + RestStatus.OK.status + ) + verifySingleConfigEquals(sesAccountConfigId, updatedSesAccountConfig, getUpdatedSesAccountResponse) + Thread.sleep(100) + + // Get all updated config + val getAllUpdatedConfigResponse = executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/configs", + "", + RestStatus.OK.status + ) + verifyMultiConfigEquals( + mapOf( + Pair(sesAccountConfigId, updatedSesAccountConfig), + Pair(emailGroupConfigId, emailGroupConfig), + Pair(emailConfigId, emailConfig) + ), + getAllUpdatedConfigResponse + ) + Thread.sleep(100) + + // Delete email notification config + val deleteResponse = executeRequest( + RestRequest.Method.DELETE.name, + "$PLUGIN_BASE_URI/configs/$emailConfigId", + "", + RestStatus.OK.status + ) + Assert.assertEquals("OK", deleteResponse.get("delete_response_list").asJsonObject.get(emailConfigId).asString) + Thread.sleep(1000) + + // Get email notification config after delete + + executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/configs/$emailConfigId", + "", + RestStatus.NOT_FOUND.status + ) + Thread.sleep(100) + } + fun `test Create email notification config without email_group IDs`() { // Create smtp account notification config val createSmtpAccountRequestJsonString = """ diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/ObjectEqualsHelpers.kt b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/ObjectEqualsHelpers.kt index f0c06b62..5735a186 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/ObjectEqualsHelpers.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/ObjectEqualsHelpers.kt @@ -35,8 +35,10 @@ import org.opensearch.commons.notifications.model.Email import org.opensearch.commons.notifications.model.EmailGroup import org.opensearch.commons.notifications.model.MethodType import org.opensearch.commons.notifications.model.NotificationConfig +import org.opensearch.commons.notifications.model.SesAccount import org.opensearch.commons.notifications.model.Slack import org.opensearch.commons.notifications.model.SmtpAccount +import org.opensearch.commons.notifications.model.Sns import org.opensearch.commons.notifications.model.Webhook fun verifyEquals(slack: Slack, jsonObject: JsonObject) { @@ -74,6 +76,17 @@ fun verifyEquals(smtpAccount: SmtpAccount, jsonObject: JsonObject) { Assert.assertEquals(smtpAccount.fromAddress, jsonObject.get("from_address").asString) } +fun verifyEquals(sesAccount: SesAccount, jsonObject: JsonObject) { + Assert.assertEquals(sesAccount.awsRegion, jsonObject.get("region").asString) + Assert.assertEquals(sesAccount.roleArn, jsonObject.get("role_arn").asString) + Assert.assertEquals(sesAccount.fromAddress, jsonObject.get("from_address").asString) +} + +fun verifyEquals(sns: Sns, jsonObject: JsonObject) { + Assert.assertEquals(sns.topicArn, jsonObject.get("topic_arn").asString) + Assert.assertEquals(sns.roleArn, jsonObject.get("role_arn").asString) +} + fun verifyEquals(config: NotificationConfig, jsonObject: JsonObject) { Assert.assertEquals(config.name, jsonObject.get("name").asString) Assert.assertEquals(config.description, jsonObject.get("description").asString) @@ -91,10 +104,15 @@ fun verifyEquals(config: NotificationConfig, jsonObject: JsonObject) { (config.configData as SmtpAccount), jsonObject.get("smtp_account").asJsonObject ) + ConfigType.SES_ACCOUNT -> verifyEquals( + (config.configData as SesAccount), + jsonObject.get("ses_account").asJsonObject + ) ConfigType.EMAIL_GROUP -> verifyEquals( (config.configData as EmailGroup), jsonObject.get("email_group").asJsonObject ) + ConfigType.SNS -> verifyEquals((config.configData as Sns), jsonObject.get("sns").asJsonObject) else -> Assert.fail("configType:${config.configType} not handled in test") } } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SesDestination.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SesDestination.kt index 9a7f307e..057378b7 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SesDestination.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SesDestination.kt @@ -19,6 +19,7 @@ import software.amazon.awssdk.regions.Region * This class holds the contents of ses destination */ class SesDestination( + val accountName: String, val awsRegion: String, val roleArn: String?, val fromAddress: String, From 6d306ffb2cb5b4403cb96ebe91d0d42942763589 Mon Sep 17 00:00:00 2001 From: Chen Dai <46505291+dai-chen@users.noreply.github.com> Date: Tue, 17 Aug 2021 09:42:17 -0700 Subject: [PATCH 19/29] Include back failed tests (#283) Signed-off-by: Chen Dai --- .github/workflows/notifications-test-and-build-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/notifications-test-and-build-workflow.yml b/.github/workflows/notifications-test-and-build-workflow.yml index daf6d600..45a6f50d 100644 --- a/.github/workflows/notifications-test-and-build-workflow.yml +++ b/.github/workflows/notifications-test-and-build-workflow.yml @@ -70,7 +70,7 @@ jobs: - name: Build with Gradle run: | cd notifications - ./gradlew build -PexcludeTests="**/SesChannelIT*, **/PluginActionTests*" -Dopensearch.version=${{ env.OPENSEARCH_VERSION }}.0 + ./gradlew build -PexcludeTests="**/SesChannelIT*" -Dopensearch.version=${{ env.OPENSEARCH_VERSION }}.0 - name: Upload coverage uses: codecov/codecov-action@v1 From 43046e214b379df55a7b4b38c8f3c6c822ae9d9d Mon Sep 17 00:00:00 2001 From: Anantha Krishna Bhatta Date: Tue, 17 Aug 2021 16:01:15 -0700 Subject: [PATCH 20/29] SES channel is migrated to AWS SDK V1 to be same as SNS client Signed-off-by: @akbhatta --- notifications/build.gradle | 2 +- notifications/spi/build.gradle | 36 ++++------- .../spi/client/DestinationSesClient.kt | 59 ++++++++++--------- .../spi/credentials/SesClientFactory.kt | 5 +- .../credentials/oss/SesClientFactoryImpl.kt | 16 ++--- .../spi/model/destination/SesDestination.kt | 4 +- 6 files changed, 58 insertions(+), 64 deletions(-) diff --git a/notifications/build.gradle b/notifications/build.gradle index 67089a27..1731bf79 100644 --- a/notifications/build.gradle +++ b/notifications/build.gradle @@ -31,7 +31,7 @@ buildscript { opensearch_version = System.getProperty("opensearch.version", "1.0.0") kotlin_version = System.getProperty("kotlin.version", "1.4.32") junit_version = System.getProperty("junit.version", "5.7.2") - aws_version = System.getProperty("aws.version", "1.12.20") + aws_version = System.getProperty("aws.version", "1.12.48") } repositories { diff --git a/notifications/spi/build.gradle b/notifications/spi/build.gradle index 9585d858..bcff3811 100644 --- a/notifications/spi/build.gradle +++ b/notifications/spi/build.gradle @@ -89,26 +89,18 @@ configurations.all { resolutionStrategy { force "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}" force "org.jetbrains.kotlin:kotlin-stdlib-common:${kotlin_version}" - force "commons-logging:commons-logging:1.2" // resolve for awssdk:ses - force "commons-codec:commons-codec:1.13" // resolve for awssdk:ses - force "io.netty:netty-codec-http:4.1.63.Final" // resolve for awssdk:ses - force "io.netty:netty-handler:4.1.63.Final" // resolve for awssdk:ses - force "io.netty:netty-common:4.1.63.Final" // resolve for awssdk:ses - force "io.netty:netty-buffer:4.1.63.Final" // resolve for awssdk:ses - force "io.netty:netty-transport:4.1.63.Final" // resolve for awssdk:ses - force "io.netty:netty-codec:4.1.63.Final" // resolve for awssdk:ses - force "org.apache.httpcomponents:httpclient:4.5.10" // resolve for awssdk:ses - force "org.apache.httpcomponents:httpcore:4.4.13" // resolve for awssdk:ses - // Resolve for awssdk:sns - force "joda-time:joda-time:2.8.1" - force "com.fasterxml.jackson.core:jackson-core:2.12.3" // resolve for awssdk:ses - force "com.fasterxml.jackson.core:jackson-annotations:2.12.3" // resolve for awssdk:ses - force "com.fasterxml.jackson.core:jackson-databind:2.12.3" // resolve for awssdk:ses - force "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.12.3" // resolve for awssdk:ses - force "com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.12.3" // resolve for awssdk:ses - force "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.12.3" // resolve for awssdk:ses - force "org.reactivestreams:reactive-streams:1.0.3" // resolve for awssdk:ses - force "junit:junit:4.12" // resolve for awssdk:ses + force "commons-logging:commons-logging:1.2" // resolve for amazonaws + force "commons-codec:commons-codec:1.13" // resolve for amazonaws + force "org.apache.httpcomponents:httpclient:4.5.10" // resolve for amazonaws + force "org.apache.httpcomponents:httpcore:4.4.13" // resolve for amazonaws + force "joda-time:joda-time:2.8.1" // Resolve for amazonaws + force "com.fasterxml.jackson.core:jackson-core:2.12.3" // resolve for amazonaws + force "com.fasterxml.jackson.core:jackson-annotations:2.12.3" // resolve for amazonaws + force "com.fasterxml.jackson.core:jackson-databind:2.12.3" // resolve for amazonaws + force "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.12.3" // resolve for amazonaws + force "com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.12.3" // resolve for amazonaws + force "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.12.3" // resolve for amazonaws + force "junit:junit:4.12" // resolve for amazonaws } } @@ -120,9 +112,7 @@ dependencies { compile "org.apache.httpcomponents:httpclient:4.5.10" compile "com.amazonaws:aws-java-sdk-sns:${aws_version}" compile "com.amazonaws:aws-java-sdk-sts:${aws_version}" - compile ("software.amazon.awssdk:ses:2.14.16") { - exclude module: 'annotations' // conflict with org.jetbrains:annotations, integTestRunner fails with error "codebase property already set" - } + compile "com.amazonaws:aws-java-sdk-ses:${aws_version}" compile "com.sun.mail:javax.mail:1.6.2" implementation "com.github.seancfoley:ipaddress:5.3.3" testImplementation( diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSesClient.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSesClient.kt index ffff5a49..819bc233 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSesClient.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSesClient.kt @@ -11,6 +11,16 @@ package org.opensearch.notifications.spi.client +import com.amazonaws.AmazonServiceException +import com.amazonaws.SdkBaseException +import com.amazonaws.services.simpleemail.model.AccountSendingPausedException +import com.amazonaws.services.simpleemail.model.AmazonSimpleEmailServiceException +import com.amazonaws.services.simpleemail.model.ConfigurationSetDoesNotExistException +import com.amazonaws.services.simpleemail.model.ConfigurationSetSendingPausedException +import com.amazonaws.services.simpleemail.model.MailFromDomainNotVerifiedException +import com.amazonaws.services.simpleemail.model.MessageRejectedException +import com.amazonaws.services.simpleemail.model.RawMessage +import com.amazonaws.services.simpleemail.model.SendRawEmailRequest import org.opensearch.notifications.spi.NotificationSpiPlugin.Companion.LOG_PREFIX import org.opensearch.notifications.spi.credentials.SesClientFactory import org.opensearch.notifications.spi.model.DestinationMessageResponse @@ -20,18 +30,8 @@ import org.opensearch.notifications.spi.setting.PluginSettings import org.opensearch.notifications.spi.utils.SecurityAccess import org.opensearch.notifications.spi.utils.logger import org.opensearch.rest.RestStatus -import software.amazon.awssdk.core.SdkBytes -import software.amazon.awssdk.core.exception.SdkException -import software.amazon.awssdk.regions.Region -import software.amazon.awssdk.services.ses.model.AccountSendingPausedException -import software.amazon.awssdk.services.ses.model.ConfigurationSetDoesNotExistException -import software.amazon.awssdk.services.ses.model.ConfigurationSetSendingPausedException -import software.amazon.awssdk.services.ses.model.MailFromDomainNotVerifiedException -import software.amazon.awssdk.services.ses.model.MessageRejectedException -import software.amazon.awssdk.services.ses.model.RawMessage -import software.amazon.awssdk.services.ses.model.SendRawEmailRequest -import software.amazon.awssdk.services.ses.model.SesException import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer import java.util.Properties import javax.mail.Session import javax.mail.internet.MimeMessage @@ -91,22 +91,16 @@ class DestinationSesClient(private val sesClientFactory: SesClientFactory) { ): DestinationMessageResponse { return try { log.debug("$LOG_PREFIX:Sending Email-SES:$referenceId") - val region = Region.of(sesAwsRegion) - val client = sesClientFactory.createSesClient(region, roleArn) + val client = sesClientFactory.createSesClient(sesAwsRegion, roleArn) val outputStream = ByteArrayOutputStream() SecurityAccess.doPrivileged { mimeMessage.writeTo(outputStream) } val emailSize = outputStream.size() if (emailSize <= PluginSettings.emailSizeLimit) { - val data = SdkBytes.fromByteArray(outputStream.toByteArray()) - val rawMessage = RawMessage.builder() - .data(data) - .build() - val rawEmailRequest = SendRawEmailRequest.builder() - .rawMessage(rawMessage) - .build() + val rawMessage = RawMessage(ByteBuffer.wrap(outputStream.toByteArray())) + val rawEmailRequest = SendRawEmailRequest(rawMessage) val response = SecurityAccess.doPrivileged { client.sendRawEmail(rawEmailRequest) } log.info("$LOG_PREFIX:Email-SES:$referenceId status:$response") - DestinationMessageResponse(RestStatus.OK.status, "Success") + DestinationMessageResponse(RestStatus.OK.status, "Success:${response.messageId}") } else { DestinationMessageResponse( RestStatus.REQUEST_ENTITY_TOO_LARGE.status, @@ -123,9 +117,11 @@ class DestinationSesClient(private val sesClientFactory: SesClientFactory) { DestinationMessageResponse(RestStatus.SERVICE_UNAVAILABLE.status, getSesExceptionText(exception)) } catch (exception: AccountSendingPausedException) { DestinationMessageResponse(RestStatus.INSUFFICIENT_STORAGE.status, getSesExceptionText(exception)) - } catch (exception: SesException) { + } catch (exception: AmazonSimpleEmailServiceException) { DestinationMessageResponse(RestStatus.FAILED_DEPENDENCY.status, getSesExceptionText(exception)) - } catch (exception: SdkException) { + } catch (exception: AmazonServiceException) { + DestinationMessageResponse(RestStatus.FAILED_DEPENDENCY.status, getServiceExceptionText(exception)) + } catch (exception: SdkBaseException) { DestinationMessageResponse(RestStatus.FAILED_DEPENDENCY.status, getSdkExceptionText(exception)) } } @@ -135,10 +131,19 @@ class DestinationSesClient(private val sesClientFactory: SesClientFactory) { * @param exception SES Exception * @return generated error string */ - private fun getSesExceptionText(exception: SesException): String { - val httpResponse = exception.awsErrorDetails().sdkHttpResponse() + private fun getSesExceptionText(exception: AmazonSimpleEmailServiceException): String { log.info("$LOG_PREFIX:SesException $exception") - return "sendEmail Error, SES status:${httpResponse.statusCode()}:${httpResponse.statusText()}" + return "sendEmail Error, SES status:${exception.errorMessage}" + } + + /** + * Create error string from Amazon Service Exceptions + * @param exception Amazon Service Exception + * @return generated error string + */ + private fun getServiceExceptionText(exception: AmazonServiceException): String { + log.info("$LOG_PREFIX:SesException $exception") + return "sendEmail Error(${exception.statusCode}), SES status:(${exception.errorType.name})${exception.errorCode}:${exception.errorMessage}" } /** @@ -146,7 +151,7 @@ class DestinationSesClient(private val sesClientFactory: SesClientFactory) { * @param exception SDK Exception * @return generated error string */ - private fun getSdkExceptionText(exception: SdkException): String { + private fun getSdkExceptionText(exception: SdkBaseException): String { log.info("$LOG_PREFIX:SdkException $exception") return "sendEmail Error, SDK status:${exception.message}" } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SesClientFactory.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SesClientFactory.kt index 23ad2877..df437ff5 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SesClientFactory.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SesClientFactory.kt @@ -10,12 +10,11 @@ */ package org.opensearch.notifications.spi.credentials -import software.amazon.awssdk.regions.Region -import software.amazon.awssdk.services.ses.SesClient +import com.amazonaws.services.simpleemail.AmazonSimpleEmailService /** * Interface for creating SES client */ interface SesClientFactory { - fun createSesClient(region: Region, roleArn: String?): SesClient + fun createSesClient(region: String, roleArn: String?): AmazonSimpleEmailService } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SesClientFactoryImpl.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SesClientFactoryImpl.kt index 6b59658f..341b0e91 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SesClientFactoryImpl.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SesClientFactoryImpl.kt @@ -10,22 +10,22 @@ */ package org.opensearch.notifications.spi.credentials.oss +import com.amazonaws.services.simpleemail.AmazonSimpleEmailService +import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClientBuilder import org.opensearch.notifications.spi.credentials.SesClientFactory import org.opensearch.notifications.spi.utils.SecurityAccess -import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider -import software.amazon.awssdk.regions.Region -import software.amazon.awssdk.services.ses.SesClient /** * Factory for creating SES client */ object SesClientFactoryImpl : SesClientFactory { - override fun createSesClient(region: Region, roleArn: String?): SesClient { + override fun createSesClient(region: String, roleArn: String?): AmazonSimpleEmailService { return SecurityAccess.doPrivileged { - // TODO: use CredentialsProviderFactory when it supports AWS SDK v2 - SesClient.builder() - .region(region) - .credentialsProvider(DefaultCredentialsProvider.create()) + val credentials = + CredentialsProviderFactory().getCredentialsProvider(region, roleArn) + AmazonSimpleEmailServiceClientBuilder.standard() + .withRegion(region) + .withCredentials(credentials) .build() } } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SesDestination.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SesDestination.kt index 057378b7..73c6aa41 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SesDestination.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SesDestination.kt @@ -11,9 +11,9 @@ package org.opensearch.notifications.spi.model.destination +import com.amazonaws.regions.Regions import org.opensearch.common.Strings import org.opensearch.notifications.spi.utils.validateEmail -import software.amazon.awssdk.regions.Region /** * This class holds the contents of ses destination @@ -28,7 +28,7 @@ class SesDestination( init { require(!Strings.isNullOrEmpty(awsRegion)) { "aws region should be provided" } - require(Region.regions().any { it.id() == awsRegion }) { "aws region is not valid" } + require(Regions.values().any { it.name == awsRegion }) { "aws region is not valid" } validateEmail(fromAddress) validateEmail(recipient) } From 816dd1de74608d7063877b7061d72518b52ec98b Mon Sep 17 00:00:00 2001 From: "Daniel Doubrovkine (dB.)" Date: Wed, 18 Aug 2021 13:28:48 -0400 Subject: [PATCH 21/29] Fix snapshot build. (#284) Signed-off-by: dblock --- .../notifications-test-and-build-workflow.yml | 12 ++++---- notifications/build.gradle | 14 +++++++++- notifications/gradle.properties | 28 ------------------- notifications/notifications/build.gradle | 3 +- 4 files changed, 20 insertions(+), 37 deletions(-) delete mode 100644 notifications/gradle.properties diff --git a/.github/workflows/notifications-test-and-build-workflow.yml b/.github/workflows/notifications-test-and-build-workflow.yml index 45a6f50d..3b3f1a0c 100644 --- a/.github/workflows/notifications-test-and-build-workflow.yml +++ b/.github/workflows/notifications-test-and-build-workflow.yml @@ -26,9 +26,9 @@ name: Test and Build Notifications on: [push, pull_request] env: - OPENSEARCH_VERSION: '1.0' - COMMON_UTILS_BRANCH: 'notification-dev' - OPENSEARCH_BRANCH: '1.0' + OPENSEARCH_VERSION: '1.1.0-SNAPSHOT' + OPENSEARCH_BRANCH: '1.x' + COMMON_UTILS_BRANCH: 'main' jobs: build: @@ -49,7 +49,7 @@ jobs: ref: ${{ env.OPENSEARCH_BRANCH }} - name: Build OpenSearch working-directory: ./OpenSearch - run: ./gradlew publishToMavenLocal -Dbuild.snapshot=false + run: ./gradlew publishToMavenLocal # dependencies: common-utils - name: Checkout common-utils @@ -60,7 +60,7 @@ jobs: path: common-utils - name: Build common-utils working-directory: ./common-utils - run: ./gradlew publishToMavenLocal -Dopensearch.version=${{ env.OPENSEARCH_VERSION }}.0 + run: ./gradlew publishToMavenLocal -Dopensearch.version=${{ env.OPENSEARCH_VERSION }} # notifications - name: Checkout Notifications @@ -70,7 +70,7 @@ jobs: - name: Build with Gradle run: | cd notifications - ./gradlew build -PexcludeTests="**/SesChannelIT*" -Dopensearch.version=${{ env.OPENSEARCH_VERSION }}.0 + ./gradlew build -PexcludeTests="**/SesChannelIT*" -Dopensearch.version=${{ env.OPENSEARCH_VERSION }} - name: Upload coverage uses: codecov/codecov-action@v1 diff --git a/notifications/build.gradle b/notifications/build.gradle index 1731bf79..b72992d8 100644 --- a/notifications/build.gradle +++ b/notifications/build.gradle @@ -28,7 +28,10 @@ buildscript { ext { - opensearch_version = System.getProperty("opensearch.version", "1.0.0") + opensearch_version = System.getProperty("opensearch.version", "1.1.0-SNAPSHOT") + // 1.0.0 -> 1.0.0.0, and 1.0.0-SNAPSHOT -> 1.0.0.0-SNAPSHOT + opensearch_build = opensearch_version.replaceAll(/(\.\d)([^\d]*)$/, '$1.0$2') + common_utils_version = System.getProperty("common_utils.version", opensearch_build) kotlin_version = System.getProperty("kotlin.version", "1.4.32") junit_version = System.getProperty("junit.version", "5.7.2") aws_version = System.getProperty("aws.version", "1.12.48") @@ -55,7 +58,16 @@ apply plugin: 'jacoco' apply plugin: 'io.gitlab.arturbosch.detekt' apply from: 'build-tools/merged-coverage.gradle' +ext { + isSnapshot = "true" == System.getProperty("build.snapshot", "true") +} + allprojects { + version = "${opensearch_version}" - "-SNAPSHOT" + ".0" + if (isSnapshot) { + version += "-SNAPSHOT" + } + repositories { mavenLocal() mavenCentral() diff --git a/notifications/gradle.properties b/notifications/gradle.properties deleted file mode 100644 index 9fe7a004..00000000 --- a/notifications/gradle.properties +++ /dev/null @@ -1,28 +0,0 @@ -# -# SPDX-License-Identifier: Apache-2.0 -# -# The OpenSearch Contributors require contributions made to -# this file be licensed under the Apache-2.0 license or a -# compatible open source license. -# -# Modifications Copyright OpenSearch Contributors. See -# GitHub history for details. -# - -# -# Copyright 2020 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. -# -# - -version = 1.0.0 diff --git a/notifications/notifications/build.gradle b/notifications/notifications/build.gradle index f0800b3a..204b24d9 100644 --- a/notifications/notifications/build.gradle +++ b/notifications/notifications/build.gradle @@ -57,7 +57,6 @@ allOpen { ext { projectSubstitutions = [:] - isSnapshot = "true" == System.getProperty("build.snapshot", "true") licenseFile = rootProject.file('LICENSE') noticeFile = rootProject.file('NOTICE') } @@ -79,7 +78,7 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}" compile "org.jetbrains.kotlin:kotlin-stdlib-common:${kotlin_version}" compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3" // ${kotlin_version} does not work for coroutines - compile "${group}:common-utils:${opensearch_version}.0" + compile "${group}:common-utils:${common_utils_version}" testImplementation( 'org.assertj:assertj-core:3.19.0', From 70264bb3aafe62336bc8c2e9697baf70c1ed383a Mon Sep 17 00:00:00 2001 From: David Cui <53581635+davidcui1225@users.noreply.github.com> Date: Wed, 18 Aug 2021 13:05:19 -0700 Subject: [PATCH 22/29] Add Backend Metrics (#268) * add backend metrics for notifications Signed-off-by: David Cui * add test cases for counters Signed-off-by: David Cui * addressed comments, refactored send message usage after merging from upstream Signed-off-by: David Cui * add channel metrics from deprecated front-end metrics collection Signed-off-by: David Cui * removed redundant metrics Signed-off-by: David Cui * refactored Metrics to kotlin, grouped compile statement in build.gradle Signed-off-by: David Cui --- notifications/notifications/build.gradle | 3 +- .../notifications/NotificationPlugin.kt | 4 +- .../action/GetFeatureChannelListAction.kt | 6 +- .../notifications/action/PluginBaseAction.kt | 10 + .../index/ConfigIndexingActions.kt | 30 +- .../index/EventIndexingActions.kt | 5 + .../notifications/metrics/BasicCounter.kt | 47 +++ .../notifications/metrics/Counter.kt | 28 ++ .../notifications/metrics/Metrics.kt | 281 ++++++++++++++++++ .../notifications/metrics/RollingCounter.kt | 89 ++++++ .../NotificationConfigRestHandler.kt | 9 + .../NotificationEventRestHandler.kt | 3 + ...tificationFeatureChannelListRestHandler.kt | 3 + .../NotificationFeaturesRestHandler.kt | 3 + .../NotificationStatsRestHandler.kt | 69 +++++ .../resthandler/PluginBaseHandler.kt | 3 + .../RestResponseToXContentListener.kt | 18 ++ .../resthandler/SendTestMessageRestHandler.kt | 3 + .../send/SendMessageActionHelper.kt | 17 +- .../metrics/BasicCounterTest.java | 39 +++ .../metrics/RollingCounterTest.java | 85 ++++++ 21 files changed, 747 insertions(+), 8 deletions(-) create mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/BasicCounter.kt create mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/Counter.kt create mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/Metrics.kt create mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/RollingCounter.kt create mode 100644 notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationStatsRestHandler.kt create mode 100644 notifications/notifications/src/test/java/org/opensearch/notifications/metrics/BasicCounterTest.java create mode 100644 notifications/notifications/src/test/java/org/opensearch/notifications/metrics/RollingCounterTest.java diff --git a/notifications/notifications/build.gradle b/notifications/notifications/build.gradle index 204b24d9..bec0f8e2 100644 --- a/notifications/notifications/build.gradle +++ b/notifications/notifications/build.gradle @@ -79,6 +79,7 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-common:${kotlin_version}" compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3" // ${kotlin_version} does not work for coroutines compile "${group}:common-utils:${common_utils_version}" + compile "org.json:json:20180813" testImplementation( 'org.assertj:assertj-core:3.19.0', @@ -101,7 +102,7 @@ dependencies { testCompile 'com.google.code.gson:gson:2.8.7' testImplementation 'org.springframework.integration:spring-integration-mail:5.5.0' testImplementation 'org.springframework.integration:spring-integration-test-support:5.5.0' - + compile group: 'com.github.wnameless', name: 'json-flattener', version: '0.1.0' compile project(path: ":${rootProject.name}-spi", configuration: 'shadow') } diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationPlugin.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationPlugin.kt index 9dc54a93..84c568f5 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationPlugin.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/NotificationPlugin.kt @@ -61,6 +61,7 @@ import org.opensearch.notifications.resthandler.NotificationConfigRestHandler import org.opensearch.notifications.resthandler.NotificationEventRestHandler import org.opensearch.notifications.resthandler.NotificationFeatureChannelListRestHandler import org.opensearch.notifications.resthandler.NotificationFeaturesRestHandler +import org.opensearch.notifications.resthandler.NotificationStatsRestHandler import org.opensearch.notifications.resthandler.SendTestMessageRestHandler import org.opensearch.notifications.security.UserAccessManager import org.opensearch.notifications.send.SendMessageActionHelper @@ -189,7 +190,8 @@ internal class NotificationPlugin : ActionPlugin, Plugin() { NotificationEventRestHandler(), NotificationFeaturesRestHandler(), NotificationFeatureChannelListRestHandler(), - SendTestMessageRestHandler() + SendTestMessageRestHandler(), + NotificationStatsRestHandler() ) } } diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/GetFeatureChannelListAction.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/GetFeatureChannelListAction.kt index 1694270b..8376f7ec 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/GetFeatureChannelListAction.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/GetFeatureChannelListAction.kt @@ -40,6 +40,7 @@ import org.opensearch.commons.notifications.action.GetFeatureChannelListResponse import org.opensearch.commons.notifications.action.NotificationsActions import org.opensearch.commons.utils.recreateObject import org.opensearch.notifications.index.ConfigIndexingActions +import org.opensearch.notifications.metrics.Metrics import org.opensearch.tasks.Task import org.opensearch.transport.TransportService @@ -80,7 +81,10 @@ internal class GetFeatureChannelListAction @Inject constructor( request: GetFeatureChannelListRequest, user: User? ): GetFeatureChannelListResponse { - require(!Strings.isNullOrEmpty(request.feature)) { "Not a valid feature" } // TODO: Validate against allowed features + require(!Strings.isNullOrEmpty(request.feature)) { + Metrics.NOTIFICATIONS_FEATURE_CHANNELS_INFO_USER_ERROR_INVALID_FEATURE_TAG.counter.increment() + "Not a valid feature" + } // TODO: Validate against allowed features return ConfigIndexingActions.getFeatureChannelList(request, user) } } diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/PluginBaseAction.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/PluginBaseAction.kt index f47a0d10..c6598d5d 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/PluginBaseAction.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/PluginBaseAction.kt @@ -46,6 +46,7 @@ import org.opensearch.index.IndexNotFoundException import org.opensearch.index.engine.VersionConflictEngineException import org.opensearch.indices.InvalidIndexNameException import org.opensearch.notifications.NotificationPlugin.Companion.LOG_PREFIX +import org.opensearch.notifications.metrics.Metrics import org.opensearch.rest.RestStatus import org.opensearch.tasks.Task import org.opensearch.transport.TransportService @@ -79,9 +80,11 @@ internal abstract class PluginBaseAction if (it.docInfo.id == email.emailAccountID) { // Email Group ID is specified as Email Account ID + Metrics.NOTIFICATIONS_CONFIG_USER_ERROR_INVALID_EMAIL_ACCOUNT_ID.counter.increment() throw OpenSearchStatusException( "configId ${it.docInfo.id} is not a valid email account ID", RestStatus.NOT_ACCEPTABLE @@ -124,6 +126,7 @@ object ConfigIndexingActions { } ConfigType.SMTP_ACCOUNT -> if (it.docInfo.id != email.emailAccountID) { // Email Account ID is specified as Email Group ID + Metrics.NOTIFICATIONS_CONFIG_USER_ERROR_INVALID_EMAIL_GROUP_ID.counter.increment() throw OpenSearchStatusException( "configId ${it.docInfo.id} is not a valid email group ID", RestStatus.NOT_ACCEPTABLE @@ -131,6 +134,7 @@ object ConfigIndexingActions { } ConfigType.SES_ACCOUNT -> if (it.docInfo.id != email.emailAccountID) { // Email Account ID is specified as Email Group ID + Metrics.NOTIFICATIONS_CONFIG_USER_ERROR_INVALID_EMAIL_GROUP_ID.counter.increment() throw OpenSearchStatusException( "configId ${it.docInfo.id} is not a valid email group ID", RestStatus.NOT_ACCEPTABLE @@ -138,6 +142,7 @@ object ConfigIndexingActions { } else -> { // Config ID is neither Email Group ID or valid Email Account ID + Metrics.NOTIFICATIONS_CONFIG_USER_ERROR_NEITHER_EMAIL_NOR_GROUP.counter.increment() throw OpenSearchStatusException( "configId ${it.docInfo.id} is not a valid email group ID or email account ID", RestStatus.NOT_ACCEPTABLE @@ -147,6 +152,7 @@ object ConfigIndexingActions { // Validate that the user has access to underlying configurations as well. val currentMetadata = it.configDoc.metadata if (!userAccess.doesUserHasAccess(user, currentMetadata.tenant, currentMetadata.access)) { + Metrics.NOTIFICATIONS_PERMISSION_USER_ERROR.counter.increment() throw OpenSearchStatusException( "Permission denied for NotificationConfig ${it.docInfo.id}", RestStatus.FORBIDDEN @@ -158,6 +164,7 @@ object ConfigIndexingActions { val missingFeatures = features.filterNot { item -> it.configDoc.config.features.contains(item) } + Metrics.NOTIFICATIONS_SECURITY_USER_ERROR.counter.increment() throw OpenSearchStatusException( "Some Features not available in NotificationConfig ${it.docInfo.id}:$missingFeatures", RestStatus.FORBIDDEN @@ -217,10 +224,13 @@ object ConfigIndexingActions { ) val configDoc = NotificationConfigDoc(metadata, request.notificationConfig) val docId = operations.createNotificationConfig(configDoc, request.configId) - docId ?: throw OpenSearchStatusException( - "NotificationConfig Creation failed", - RestStatus.INTERNAL_SERVER_ERROR - ) + docId ?: run { + Metrics.NOTIFICATIONS_CONFIG_CREATE_SYSTEM_ERROR.counter.increment() + throw OpenSearchStatusException( + "NotificationConfig Creation failed", + RestStatus.INTERNAL_SERVER_ERROR + ) + } return CreateNotificationConfigResponse(docId) } @@ -237,6 +247,7 @@ object ConfigIndexingActions { val currentConfigDoc = operations.getNotificationConfig(request.configId) currentConfigDoc ?: run { + Metrics.NOTIFICATIONS_CONFIG_UPDATE_USER_ERROR_INVALID_CONFIG_ID.counter.increment() throw OpenSearchStatusException( "NotificationConfig ${request.configId} not found", RestStatus.NOT_FOUND @@ -245,6 +256,7 @@ object ConfigIndexingActions { val currentMetadata = currentConfigDoc.configDoc.metadata if (!userAccess.doesUserHasAccess(user, currentMetadata.tenant, currentMetadata.access)) { + Metrics.NOTIFICATIONS_PERMISSION_USER_ERROR.counter.increment() throw OpenSearchStatusException( "Permission denied for NotificationConfig ${request.configId}", RestStatus.FORBIDDEN @@ -257,6 +269,7 @@ object ConfigIndexingActions { val newMetadata = currentMetadata.copy(lastUpdateTime = Instant.now()) val newConfigData = NotificationConfigDoc(newMetadata, request.notificationConfig) if (!operations.updateNotificationConfig(request.configId, newConfigData)) { + Metrics.NOTIFICATIONS_CONFIG_UPDATE_SYSTEM_ERROR.counter.increment() throw OpenSearchStatusException("NotificationConfig Update failed", RestStatus.INTERNAL_SERVER_ERROR) } return UpdateNotificationConfigResponse(request.configId) @@ -289,10 +302,12 @@ object ConfigIndexingActions { val configDoc = operations.getNotificationConfig(configId) configDoc ?: run { + Metrics.NOTIFICATIONS_CONFIG_INFO_USER_ERROR_INVALID_CONFIG_ID.counter.increment() throw OpenSearchStatusException("NotificationConfig $configId not found", RestStatus.NOT_FOUND) } val metadata = configDoc.configDoc.metadata if (!userAccess.doesUserHasAccess(user, metadata.tenant, metadata.access)) { + Metrics.NOTIFICATIONS_PERMISSION_USER_ERROR.counter.increment() throw OpenSearchStatusException("Permission denied for NotificationConfig $configId", RestStatus.FORBIDDEN) } val configInfo = NotificationConfigInfo( @@ -317,6 +332,7 @@ object ConfigIndexingActions { if (configDocs.size != configIds.size) { val mutableSet = configIds.toMutableSet() configDocs.forEach { mutableSet.remove(it.docInfo.id) } + Metrics.NOTIFICATIONS_CONFIG_INFO_USER_ERROR_SET_NOT_FOUND.counter.increment() throw OpenSearchStatusException( "NotificationConfig $mutableSet not found", RestStatus.NOT_FOUND @@ -325,6 +341,7 @@ object ConfigIndexingActions { configDocs.forEach { val currentMetadata = it.configDoc.metadata if (!userAccess.doesUserHasAccess(user, currentMetadata.tenant, currentMetadata.access)) { + Metrics.NOTIFICATIONS_PERMISSION_USER_ERROR.counter.increment() throw OpenSearchStatusException( "Permission denied for NotificationConfig ${it.docInfo.id}", RestStatus.FORBIDDEN @@ -410,6 +427,7 @@ object ConfigIndexingActions { val currentConfigDoc = operations.getNotificationConfig(configId) currentConfigDoc ?: run { + Metrics.NOTIFICATIONS_CONFIG_DELETE_USER_ERROR_INVALID_CONFIG_ID.counter.increment() throw OpenSearchStatusException( "NotificationConfig $configId not found", RestStatus.NOT_FOUND @@ -418,12 +436,14 @@ object ConfigIndexingActions { val currentMetadata = currentConfigDoc.configDoc.metadata if (!userAccess.doesUserHasAccess(user, currentMetadata.tenant, currentMetadata.access)) { + Metrics.NOTIFICATIONS_PERMISSION_USER_ERROR.counter.increment() throw OpenSearchStatusException( "Permission denied for NotificationConfig $configId", RestStatus.FORBIDDEN ) } if (!operations.deleteNotificationConfig(configId)) { + Metrics.NOTIFICATIONS_CONFIG_DELETE_SYSTEM_ERROR.counter.increment() throw OpenSearchStatusException( "NotificationConfig $configId delete failed", RestStatus.REQUEST_TIMEOUT @@ -445,6 +465,7 @@ object ConfigIndexingActions { if (configDocs.size != configIds.size) { val mutableSet = configIds.toMutableSet() configDocs.forEach { mutableSet.remove(it.docInfo.id) } + Metrics.NOTIFICATIONS_CONFIG_DELETE_USER_ERROR_SET_NOT_FOUND.counter.increment() throw OpenSearchStatusException( "NotificationConfig $mutableSet not found", RestStatus.NOT_FOUND @@ -453,6 +474,7 @@ object ConfigIndexingActions { configDocs.forEach { val currentMetadata = it.configDoc.metadata if (!userAccess.doesUserHasAccess(user, currentMetadata.tenant, currentMetadata.access)) { + Metrics.NOTIFICATIONS_PERMISSION_USER_ERROR.counter.increment() throw OpenSearchStatusException( "Permission denied for NotificationConfig ${it.docInfo.id}", RestStatus.FORBIDDEN diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/EventIndexingActions.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/EventIndexingActions.kt index 29386307..99f6841d 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/EventIndexingActions.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/EventIndexingActions.kt @@ -35,6 +35,7 @@ import org.opensearch.commons.notifications.model.NotificationEventInfo import org.opensearch.commons.notifications.model.NotificationEventSearchResult import org.opensearch.commons.utils.logger import org.opensearch.notifications.NotificationPlugin.Companion.LOG_PREFIX +import org.opensearch.notifications.metrics.Metrics import org.opensearch.notifications.security.UserAccess import org.opensearch.rest.RestStatus @@ -79,10 +80,12 @@ object EventIndexingActions { val eventDoc = operations.getNotificationEvent(eventId) eventDoc ?: run { + Metrics.NOTIFICATIONS_EVENTS_INFO_USER_ERROR_INVALID_CONFIG_ID.counter.increment() throw OpenSearchStatusException("NotificationEvent $eventId not found", RestStatus.NOT_FOUND) } val metadata = eventDoc.eventDoc.metadata if (!userAccess.doesUserHasAccess(user, metadata.tenant, metadata.access)) { + Metrics.NOTIFICATIONS_PERMISSION_USER_ERROR.counter.increment() throw OpenSearchStatusException("Permission denied for NotificationEvent $eventId", RestStatus.FORBIDDEN) } val eventInfo = NotificationEventInfo( @@ -107,6 +110,7 @@ object EventIndexingActions { if (eventDocs.size != eventIds.size) { val mutableSet = eventIds.toMutableSet() eventDocs.forEach { mutableSet.remove(it.docInfo.id) } + Metrics.NOTIFICATIONS_EVENTS_INFO_SYSTEM_ERROR.counter.increment() throw OpenSearchStatusException( "NotificationEvent $mutableSet not found", RestStatus.NOT_FOUND @@ -115,6 +119,7 @@ object EventIndexingActions { eventDocs.forEach { val currentMetadata = it.eventDoc.metadata if (!userAccess.doesUserHasAccess(user, currentMetadata.tenant, currentMetadata.access)) { + Metrics.NOTIFICATIONS_PERMISSION_USER_ERROR.counter.increment() throw OpenSearchStatusException( "Permission denied for NotificationEvent ${it.docInfo.id}", RestStatus.FORBIDDEN diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/BasicCounter.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/BasicCounter.kt new file mode 100644 index 00000000..b7c7ad45 --- /dev/null +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/BasicCounter.kt @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.metrics + +import java.util.concurrent.atomic.LongAdder + +/** + * Counter to hold accumulative value over time. + */ +class BasicCounter : Counter { + private val count = LongAdder() + + /** + * {@inheritDoc} + */ + override fun increment() { + count.increment() + } + + /** + * {@inheritDoc} + */ + override fun add(n: Long) { + count.add(n) + } + + /** + * {@inheritDoc} + */ + override fun getValue(): Long { + return count.toLong() + } + + /** Reset the count value to zero */ + override fun reset() { + count.reset() + } +} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/Counter.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/Counter.kt new file mode 100644 index 00000000..554cca72 --- /dev/null +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/Counter.kt @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.notifications.metrics + +/** + * Defines a generic counter. + */ +interface Counter { + /** Increments the count value by 1 unit */ + fun increment() + + /** Increments the count value by n unit */ + fun add(n: Long) + + /** Retrieves the count value accumulated up to this call */ + fun getValue(): Long + + /** Resets the count value to initial value when Counter is created */ + fun reset() +} 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 new file mode 100644 index 00000000..cd26a501 --- /dev/null +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/Metrics.kt @@ -0,0 +1,281 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.notifications.metrics + +import com.github.wnameless.json.unflattener.JsonUnflattener +import org.json.JSONObject + +/** + * Enum to hold all the metrics that need to be logged into _plugins/_notifications/local/stats API + */ +enum class Metrics(val metricName: String, val counter: Counter<*>) { + REQUEST_TOTAL("request_total", BasicCounter()), REQUEST_INTERVAL_COUNT( + "request_count", + RollingCounter() + ), + REQUEST_SUCCESS("success_count", RollingCounter()), REQUEST_USER_ERROR( + "failed_request_count_user_error", + RollingCounter() + ), + REQUEST_SYSTEM_ERROR("failed_request_count_system_error", RollingCounter()), + + /** + * Exceptions from: + * @see org.opensearch.notifications.action.PluginBaseAction + */ + NOTIFICATIONS_EXCEPTIONS_OS_STATUS_EXCEPTION( + "exception.os_status", + RollingCounter() + ), + NOTIFICATIONS_EXCEPTIONS_OS_SECURITY_EXCEPTION( + "exception.os_security", + RollingCounter() + ), + NOTIFICATIONS_EXCEPTIONS_VERSION_CONFLICT_ENGINE_EXCEPTION( + "exception.version_conflict_engine", RollingCounter() + ), + NOTIFICATIONS_EXCEPTIONS_INDEX_NOT_FOUND_EXCEPTION( + "exception.index_not_found", + RollingCounter() + ), + NOTIFICATIONS_EXCEPTIONS_INVALID_INDEX_NAME_EXCEPTION( + "exception.invalid_index_name", + RollingCounter() + ), + NOTIFICATIONS_EXCEPTIONS_ILLEGAL_ARGUMENT_EXCEPTION( + "exception.illegal_argument", + RollingCounter() + ), + NOTIFICATIONS_EXCEPTIONS_ILLEGAL_STATE_EXCEPTION( + "exception.illegal_state", + RollingCounter() + ), + NOTIFICATIONS_EXCEPTIONS_IO_EXCEPTION( + "exception.io", + RollingCounter() + ), + NOTIFICATIONS_EXCEPTIONS_INTERNAL_SERVER_ERROR( + "exception.internal_server_error", + RollingCounter() + ), // ==== Per REST endpoint metrics ==== // + + // Config Endpoints + // POST _plugins/_notifications/configs, Create a new notification config + NOTIFICATIONS_CONFIG_CREATE_TOTAL( + "notifications_config.create.total", + BasicCounter() + ), + NOTIFICATIONS_CONFIG_CREATE_INTERVAL_COUNT( + "notifications_config.create.count", + RollingCounter() + ), + NOTIFICATIONS_CONFIG_CREATE_SYSTEM_ERROR( + "notifications_config.create.system_error", + RollingCounter() + ), // PUT _plugins/_notifications/configs/{configId}, Update a notification config + NOTIFICATIONS_CONFIG_UPDATE_TOTAL( + "notifications_config.update.total", + BasicCounter() + ), + NOTIFICATIONS_CONFIG_UPDATE_INTERVAL_COUNT( + "notifications_config.update.count", + RollingCounter() + ), + NOTIFICATIONS_CONFIG_UPDATE_USER_ERROR_INVALID_CONFIG_ID( + "notifications_config.update.user_error.invalid_config_id", RollingCounter() + ), + NOTIFICATIONS_CONFIG_UPDATE_SYSTEM_ERROR( + "notifications_config.update.system_error", + RollingCounter() + ), // Notification config general user error + NOTIFICATIONS_CONFIG_USER_ERROR_INVALID_EMAIL_ACCOUNT_ID( + "notifications_config.user_error.invalid_email_account_id", RollingCounter() + ), + NOTIFICATIONS_CONFIG_USER_ERROR_INVALID_EMAIL_GROUP_ID( + "notifications_config.user_error.invalid_email_group_id", RollingCounter() + ), + NOTIFICATIONS_CONFIG_USER_ERROR_NEITHER_EMAIL_NOR_GROUP( + "notifications_config.user_error.neither_email_nor_group", RollingCounter() + ), // DELETE _plugins/_notifications/configs/{configId}, Delete a notification config + NOTIFICATIONS_CONFIG_DELETE_TOTAL( + "notifications_config.delete.total", + BasicCounter() + ), + NOTIFICATIONS_CONFIG_DELETE_INTERVAL_COUNT( + "notifications_config.delete.count", + RollingCounter() + ), + NOTIFICATIONS_CONFIG_DELETE_USER_ERROR_INVALID_CONFIG_ID( + "notifications_config.delete.user_error.invalid_config_id", RollingCounter() + ), + NOTIFICATIONS_CONFIG_DELETE_USER_ERROR_SET_NOT_FOUND( + "notifications_config.delete.user_error.set_not_found", RollingCounter() + ), + NOTIFICATIONS_CONFIG_DELETE_SYSTEM_ERROR( + "notifications_config.delete.system_error", + RollingCounter() + ), // GET _plugins/_notifications/configs/{configId} + NOTIFICATIONS_CONFIG_INFO_TOTAL( + "notifications_config.info.total", + BasicCounter() + ), + NOTIFICATIONS_CONFIG_INFO_INTERVAL_COUNT( + "notifications_config.info.count", + RollingCounter() + ), // add specific user errors for config GET operations + NOTIFICATIONS_CONFIG_INFO_USER_ERROR_INVALID_CONFIG_ID( + "notifications_config.info.user_error.invalid_config_id", RollingCounter() + ), + NOTIFICATIONS_CONFIG_INFO_USER_ERROR_SET_NOT_FOUND( + "notifications_config.info.user_error.set_not_found", RollingCounter() + ), + NOTIFICATIONS_CONFIG_INFO_SYSTEM_ERROR( + "notifications_config.info.system_error", + RollingCounter() + ), + // Event Endpoints + // GET _plugins/_notifications/events/{configId} + NOTIFICATIONS_EVENTS_INFO_TOTAL( + "notifications_events.info.total", + BasicCounter() + ), + NOTIFICATIONS_EVENTS_INFO_INTERVAL_COUNT( + "notifications_events.info.count", + RollingCounter() + ), + NOTIFICATIONS_EVENTS_INFO_USER_ERROR_INVALID_CONFIG_ID( + "notifications_events.info.user_error.invalid_config_id", RollingCounter() + ), + NOTIFICATIONS_EVENTS_INFO_SYSTEM_ERROR( + "notifications_events.info.system_error", RollingCounter() + ), + // Feature Channels Endpoints + // GET _plugins/_notifications/feature/channels/{featureTag} + NOTIFICATIONS_FEATURE_CHANNELS_INFO_TOTAL( + "notifications_feature_channels.info.total", + BasicCounter() + ), + NOTIFICATIONS_FEATURE_CHANNELS_INFO_INTERVAL_COUNT( + "notifications_feature_channels.info.count", RollingCounter() + ), + NOTIFICATIONS_FEATURE_CHANNELS_INFO_USER_ERROR_INVALID_FEATURE_TAG( + "notifications_feature_channels.info.user_error.invalid_feature_tag", RollingCounter() + ), + NOTIFICATIONS_FEATURE_CHANNELS_INFO_SYSTEM_ERROR( + "notifications_feature_channels.info.system_error", RollingCounter() + ), + // Features Endpoints + // GET _plugins/_notifications/features + NOTIFICATIONS_FEATURES_INFO_TOTAL( + "notifications_features.info.total", + BasicCounter() + ), + NOTIFICATIONS_FEATURES_INFO_INTERVAL_COUNT( + "notifications_features.info.count", + RollingCounter() + ), + NOTIFICATIONS_FEATURES_INFO_SYSTEM_ERROR( + "notifications_features.info.system_error", + RollingCounter() + ), + // Send Message Endpoints + // POST _plugins/_notifications/send + NOTIFICATIONS_SEND_MESSAGE_TOTAL( + "notifications.send_message.total", + BasicCounter() + ), + NOTIFICATIONS_SEND_MESSAGE_INTERVAL_COUNT( + "notifications.send_message.count", + RollingCounter() + ), // user errors for send message? + NOTIFICATIONS_SEND_MESSAGE_USER_ERROR_NOT_FOUND( + "notifications.send_message.user_error.not_found", RollingCounter() + ), + NOTIFICATIONS_SEND_MESSAGE_SYSTEM_ERROR( + "notifications.send_message.system_error", + RollingCounter() + ), // Track message destinations + NOTIFICATIONS_MESSAGE_DESTINATION_SLACK( + "notifications.message_destination.slack", + BasicCounter() + ), + NOTIFICATIONS_MESSAGE_DESTINATION_CHIME( + "notifications.message_destination.chime", + BasicCounter() + ), + NOTIFICATIONS_MESSAGE_DESTINATION_WEBHOOK( + "notifications.message_destination.webhook", + BasicCounter() + ), + NOTIFICATIONS_MESSAGE_DESTINATION_EMAIL( + "notifications.message_destination.email", + BasicCounter() + ), + NOTIFICATIONS_MESSAGE_DESTINATION_SES_ACCOUNT( + "notifications.message_destination.ses_account", BasicCounter() + ), + NOTIFICATIONS_MESSAGE_DESTINATION_SMTP_ACCOUNT( + "notifications.message_destination.smtp_account", BasicCounter() + ), + NOTIFICATIONS_MESSAGE_DESTINATION_EMAIL_GROUP( + "notifications.message_destination.email_group", BasicCounter() + ), // TODO: add after implementation added + NOTIFICATIONS_MESSAGE_DESTINATION_SNS( + "notifications.message_destination.sns", + BasicCounter() + ), + // Send Test Message Endpoints + // GET _plugins/_notifications/feature/test/{configId} + NOTIFICATIONS_SEND_TEST_MESSAGE_TOTAL( + "notifications.send_test_message.total", + BasicCounter() + ), + NOTIFICATIONS_SEND_TEST_MESSAGE_INTERVAL_COUNT( + "notifications.send_test_message.interval_count", RollingCounter() + ), // Send test message exceptions are thrown by the Send Message Action + NOTIFICATIONS_SECURITY_USER_ERROR( + "security_user_error", + RollingCounter() + ), + NOTIFICATIONS_PERMISSION_USER_ERROR("permissions_user_error", RollingCounter()); + + companion object { + private val values = values() + + /** + * Converts the enum metric values to JSON string + */ + fun collectToJSON(): String { + val metricsJSONObject = JSONObject() + for (metric in values) { + metricsJSONObject.put(metric.metricName, metric.counter.getValue()) + } + return metricsJSONObject.toString() + } + + /** + * Unflattens the JSON to nested JSON for easy readability and parsing + * The metric name is unflattened in the output JSON on the period '.' delimiter + * + * For ex: { "a.b.c_d" : 2 } becomes + * { + * "a" : { + * "b" : { + * "c_d" : 2 + * } + * } + * } + */ + fun collectToFlattenedJSON(): String { + return JsonUnflattener.unflatten(collectToJSON()) + } + } +} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/RollingCounter.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/RollingCounter.kt new file mode 100644 index 00000000..023dfeff --- /dev/null +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/RollingCounter.kt @@ -0,0 +1,89 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.notifications.metrics + +import java.time.Clock +import java.util.concurrent.ConcurrentSkipListMap +import kotlin.jvm.JvmOverloads + +/** + * Rolling counter. The count is refreshed every interval. In every interval the count is cumulative. + */ +class RollingCounter @JvmOverloads constructor( + private val window: Long = METRICS_ROLLING_WINDOW_VALUE, + private val interval: Long = METRICS_ROLLING_INTERVAL_VALUE, + private val clock: Clock = Clock.systemDefaultZone() +) : Counter { + private val capacity: Long = window / interval * 2 + private val timeToCountMap = ConcurrentSkipListMap() + + /** + * {@inheritDoc} + */ + override fun increment() { + add(1L) + } + + /** + * {@inheritDoc} + */ + override fun add(n: Long) { + trim() + timeToCountMap.compute(getKey(clock.millis())) { k: Long?, v: Long? -> if (v == null) n else v + n } + } + + /** + * {@inheritDoc} + */ + override fun getValue(): Long { + return getValue(getPreKey(clock.millis())) + } + + /** + * {@inheritDoc} + */ + fun getValue(key: Long): Long { + return timeToCountMap[key] ?: return 0 + } + + private fun trim() { + if (timeToCountMap.size > capacity) { + timeToCountMap.headMap(getKey(clock.millis() - window * 1000)).clear() + } + } + + private fun getKey(millis: Long): Long { + return millis / 1000 / interval + } + + private fun getPreKey(millis: Long): Long { + return getKey(millis) - 1 + } + + /** + * Number of existing intervals + */ + fun size(): Int { + return timeToCountMap.size + } + + /** + * Remove all the items from counter + */ + override fun reset() { + timeToCountMap.clear() + } + + companion object { + private const val METRICS_ROLLING_WINDOW_VALUE = 3600L + private const val METRICS_ROLLING_INTERVAL_VALUE = 60L + } +} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationConfigRestHandler.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationConfigRestHandler.kt index 2475e0bb..fb2163c1 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationConfigRestHandler.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationConfigRestHandler.kt @@ -44,6 +44,7 @@ import org.opensearch.commons.utils.logger import org.opensearch.notifications.NotificationPlugin.Companion.LOG_PREFIX import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI import org.opensearch.notifications.index.ConfigQueryHelper +import org.opensearch.notifications.metrics.Metrics import org.opensearch.rest.BaseRestHandler.RestChannelConsumer import org.opensearch.rest.BytesRestResponse import org.opensearch.rest.RestHandler.Route @@ -190,6 +191,8 @@ internal class NotificationConfigRestHandler : PluginBaseHandler() { request: RestRequest, client: NodeClient ) = RestChannelConsumer { + Metrics.NOTIFICATIONS_CONFIG_UPDATE_TOTAL.counter.increment() + Metrics.NOTIFICATIONS_CONFIG_UPDATE_INTERVAL_COUNT.counter.increment() NotificationsPluginInterface.updateNotificationConfig( client, UpdateNotificationConfigRequest.parse( @@ -204,6 +207,8 @@ internal class NotificationConfigRestHandler : PluginBaseHandler() { request: RestRequest, client: NodeClient ) = RestChannelConsumer { + Metrics.NOTIFICATIONS_CONFIG_CREATE_TOTAL.counter.increment() + Metrics.NOTIFICATIONS_CONFIG_CREATE_INTERVAL_COUNT.counter.increment() NotificationsPluginInterface.createNotificationConfig( client, CreateNotificationConfigRequest.parse(request.contentParserNextToken()), @@ -215,6 +220,8 @@ internal class NotificationConfigRestHandler : PluginBaseHandler() { request: RestRequest, client: NodeClient ): RestChannelConsumer { + Metrics.NOTIFICATIONS_CONFIG_INFO_TOTAL.counter.increment() + Metrics.NOTIFICATIONS_CONFIG_INFO_INTERVAL_COUNT.counter.increment() val configId: String? = request.param(CONFIG_ID_TAG) val configIdList: String? = request.param(CONFIG_ID_LIST_TAG) val sortField: String? = request.param(SORT_FIELD_TAG) @@ -266,6 +273,8 @@ internal class NotificationConfigRestHandler : PluginBaseHandler() { request: RestRequest, client: NodeClient ): RestChannelConsumer { + Metrics.NOTIFICATIONS_CONFIG_DELETE_TOTAL.counter.increment() + Metrics.NOTIFICATIONS_CONFIG_DELETE_INTERVAL_COUNT.counter.increment() val configId: String? = request.param(CONFIG_ID_TAG) val configIdSet: Set = request.paramAsStringArray(CONFIG_ID_LIST_TAG, arrayOf(configId)) diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationEventRestHandler.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationEventRestHandler.kt index cf7f5a5f..b444e96e 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationEventRestHandler.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationEventRestHandler.kt @@ -40,6 +40,7 @@ import org.opensearch.commons.utils.logger import org.opensearch.notifications.NotificationPlugin.Companion.LOG_PREFIX import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI import org.opensearch.notifications.index.EventQueryHelper +import org.opensearch.notifications.metrics.Metrics import org.opensearch.rest.BaseRestHandler.RestChannelConsumer import org.opensearch.rest.BytesRestResponse import org.opensearch.rest.RestHandler.Route @@ -144,6 +145,8 @@ internal class NotificationEventRestHandler : PluginBaseHandler() { request: RestRequest, client: NodeClient ): RestChannelConsumer { + Metrics.NOTIFICATIONS_EVENTS_INFO_TOTAL.counter.increment() + Metrics.NOTIFICATIONS_EVENTS_INFO_INTERVAL_COUNT.counter.increment() val eventId: String? = request.param(EVENT_ID_TAG) val eventIdList: String? = request.param(EVENT_ID_LIST_TAG) val sortField: String? = request.param(SORT_FIELD_TAG) diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationFeatureChannelListRestHandler.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationFeatureChannelListRestHandler.kt index 6f99f181..b947fd93 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationFeatureChannelListRestHandler.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationFeatureChannelListRestHandler.kt @@ -16,6 +16,7 @@ import org.opensearch.commons.notifications.NotificationConstants.FEATURE_TAG import org.opensearch.commons.notifications.NotificationsPluginInterface import org.opensearch.commons.notifications.action.GetFeatureChannelListRequest import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI +import org.opensearch.notifications.metrics.Metrics import org.opensearch.rest.BaseRestHandler.RestChannelConsumer import org.opensearch.rest.BytesRestResponse import org.opensearch.rest.RestHandler.Route @@ -70,6 +71,8 @@ internal class NotificationFeatureChannelListRestHandler : PluginBaseHandler() { override fun executeRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { return when (request.method()) { GET -> { + Metrics.NOTIFICATIONS_FEATURE_CHANNELS_INFO_TOTAL.counter.increment() + Metrics.NOTIFICATIONS_FEATURE_CHANNELS_INFO_INTERVAL_COUNT.counter.increment() val feature = request.param(FEATURE_TAG) RestChannelConsumer { NotificationsPluginInterface.getFeatureChannelList( diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationFeaturesRestHandler.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationFeaturesRestHandler.kt index 007b363c..f873fd30 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationFeaturesRestHandler.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationFeaturesRestHandler.kt @@ -15,6 +15,7 @@ import org.opensearch.client.node.NodeClient import org.opensearch.commons.notifications.NotificationsPluginInterface import org.opensearch.commons.notifications.action.GetPluginFeaturesRequest import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI +import org.opensearch.notifications.metrics.Metrics import org.opensearch.rest.BaseRestHandler.RestChannelConsumer import org.opensearch.rest.BytesRestResponse import org.opensearch.rest.RestHandler.Route @@ -69,6 +70,8 @@ internal class NotificationFeaturesRestHandler : PluginBaseHandler() { override fun executeRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { return when (request.method()) { GET -> RestChannelConsumer { + Metrics.NOTIFICATIONS_FEATURES_INFO_TOTAL.counter.increment() + Metrics.NOTIFICATIONS_FEATURES_INFO_INTERVAL_COUNT.counter.increment() NotificationsPluginInterface.getPluginFeatures( client, GetPluginFeaturesRequest(), diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationStatsRestHandler.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationStatsRestHandler.kt new file mode 100644 index 00000000..976eeb72 --- /dev/null +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationStatsRestHandler.kt @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.resthandler + +import org.opensearch.client.node.NodeClient +import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI +import org.opensearch.notifications.metrics.Metrics +import org.opensearch.rest.BaseRestHandler +import org.opensearch.rest.BaseRestHandler.RestChannelConsumer +import org.opensearch.rest.BytesRestResponse +import org.opensearch.rest.RestHandler.Route +import org.opensearch.rest.RestRequest +import org.opensearch.rest.RestRequest.Method.GET +import org.opensearch.rest.RestStatus + +/** + * Rest handler for getting notifications backend stats + */ +internal class NotificationStatsRestHandler : BaseRestHandler() { + companion object { + private const val NOTIFICATION_STATS_ACTION = "notification_stats" + private const val NOTIFICATION_STATS_URL = "$PLUGIN_BASE_URI/_local/stats" + } + + /** + * {@inheritDoc} + */ + override fun getName(): String { + return NOTIFICATION_STATS_ACTION + } + + /** + * {@inheritDoc} + */ + override fun routes(): List { + return listOf() + } + + /** + * {@inheritDoc} + */ + override fun responseParams(): Set { + return setOf() + } + + /** + * {@inheritDoc} + */ + override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { + return when (request.method()) { + // TODO: Wrap this into TransportAction + GET -> RestChannelConsumer { + it.sendResponse(BytesRestResponse(RestStatus.OK, Metrics.collectToFlattenedJSON())) + } + else -> RestChannelConsumer { + it.sendResponse(BytesRestResponse(RestStatus.METHOD_NOT_ALLOWED, "${request.method()} is not allowed")) + } + } + } +} diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/PluginBaseHandler.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/PluginBaseHandler.kt index 5ca5880c..c564ce7f 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/PluginBaseHandler.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/PluginBaseHandler.kt @@ -27,6 +27,7 @@ package org.opensearch.notifications.resthandler import org.opensearch.client.node.NodeClient +import org.opensearch.notifications.metrics.Metrics import org.opensearch.rest.BaseRestHandler import org.opensearch.rest.RestRequest @@ -39,6 +40,8 @@ abstract class PluginBaseHandler : BaseRestHandler() { * {@inheritDoc} */ override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { + Metrics.REQUEST_TOTAL.counter.increment() + Metrics.REQUEST_INTERVAL_COUNT.counter.increment() return executeRequest(request, client) } diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/RestResponseToXContentListener.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/RestResponseToXContentListener.kt index ab5bf5e1..1b6ea5b6 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/RestResponseToXContentListener.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/RestResponseToXContentListener.kt @@ -27,8 +27,12 @@ package org.opensearch.notifications.resthandler +import org.opensearch.common.xcontent.XContentBuilder import org.opensearch.commons.notifications.action.BaseResponse +import org.opensearch.notifications.metrics.Metrics +import org.opensearch.rest.BytesRestResponse import org.opensearch.rest.RestChannel +import org.opensearch.rest.RestResponse import org.opensearch.rest.RestStatus import org.opensearch.rest.action.RestToXContentListener @@ -39,6 +43,20 @@ import org.opensearch.rest.action.RestToXContentListener */ internal class RestResponseToXContentListener(channel: RestChannel) : RestToXContentListener(channel) { + override fun buildResponse(response: Response, builder: XContentBuilder?): RestResponse { + super.buildResponse(response, builder) + + Metrics.REQUEST_TOTAL.counter.increment() + Metrics.REQUEST_INTERVAL_COUNT.counter.increment() + + when (response.getStatus()) { + in RestStatus.OK..RestStatus.MULTI_STATUS -> Metrics.REQUEST_SUCCESS.counter.increment() + RestStatus.FORBIDDEN -> Metrics.NOTIFICATIONS_SECURITY_USER_ERROR.counter.increment() + in RestStatus.UNAUTHORIZED..RestStatus.TOO_MANY_REQUESTS -> Metrics.REQUEST_USER_ERROR.counter.increment() + else -> Metrics.REQUEST_SYSTEM_ERROR.counter.increment() + } + return BytesRestResponse(getStatus(response), builder) + } /** * {@inheritDoc} */ diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/SendTestMessageRestHandler.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/SendTestMessageRestHandler.kt index a00a97ec..18b342f3 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/SendTestMessageRestHandler.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/SendTestMessageRestHandler.kt @@ -19,6 +19,7 @@ import org.opensearch.commons.notifications.model.ChannelMessage import org.opensearch.commons.notifications.model.EventSource import org.opensearch.commons.notifications.model.SeverityType import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI +import org.opensearch.notifications.metrics.Metrics import org.opensearch.rest.BaseRestHandler.RestChannelConsumer import org.opensearch.rest.BytesRestResponse import org.opensearch.rest.RestHandler.Route @@ -83,6 +84,8 @@ internal class SendTestMessageRestHandler : PluginBaseHandler() { request: RestRequest, client: NodeClient ) = RestChannelConsumer { + Metrics.NOTIFICATIONS_SEND_TEST_MESSAGE_TOTAL.counter.increment() + Metrics.NOTIFICATIONS_SEND_TEST_MESSAGE_INTERVAL_COUNT.counter.increment() val feature = request.param(FEATURE_TAG) val configId = request.param(CONFIG_ID_TAG) val source = generateEventSource(feature, configId) 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 e34868bc..a97451b3 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 @@ -44,6 +44,7 @@ import org.opensearch.commons.utils.logger import org.opensearch.notifications.NotificationPlugin.Companion.LOG_PREFIX import org.opensearch.notifications.index.ConfigOperations import org.opensearch.notifications.index.EventOperations +import org.opensearch.notifications.metrics.Metrics import org.opensearch.notifications.model.DocMetadata import org.opensearch.notifications.model.NotificationConfigDocInfo import org.opensearch.notifications.model.NotificationEventDoc @@ -103,7 +104,10 @@ object SendMessageActionHelper { val event = NotificationEvent(eventSource, eventStatusList) val eventDoc = NotificationEventDoc(docMetadata, event) val docId = eventOperations.createNotificationEvent(eventDoc) - ?: throw OpenSearchStatusException("Indexing not Acknowledged", RestStatus.INSUFFICIENT_STORAGE) + ?: run { + Metrics.NOTIFICATIONS_SEND_MESSAGE_SYSTEM_ERROR.counter.increment() + throw OpenSearchStatusException("Indexing not Acknowledged", RestStatus.INSUFFICIENT_STORAGE) + } return SendNotificationResponse(docId) } @@ -177,6 +181,8 @@ object SendMessageActionHelper { childConfigs: List, message: MessageContent ): EventStatus { + Metrics.NOTIFICATIONS_SEND_MESSAGE_TOTAL.counter.increment() + Metrics.NOTIFICATIONS_SEND_MESSAGE_INTERVAL_COUNT.counter.increment() val configType = channel.configDoc.config.configType val configData = channel.configDoc.config.configData var emailRecipientStatus = listOf() @@ -220,6 +226,7 @@ object SendMessageActionHelper { } return if (response == null) { log.warn("Cannot send message to destination for config id :${channel.docInfo.id}") + Metrics.NOTIFICATIONS_SEND_MESSAGE_USER_ERROR_NOT_FOUND.counter.increment() eventStatus.copy(deliveryStatus = DeliveryStatus(RestStatus.NOT_FOUND.name, "Channel not found")) } else { response @@ -285,6 +292,7 @@ object SendMessageActionHelper { return if (!channel.configDoc.config.isEnabled) { DeliveryStatus(RestStatus.LOCKED.name, "The channel is muted") } else if (!channel.configDoc.config.features.contains(eventSource.feature)) { + Metrics.NOTIFICATIONS_PERMISSION_USER_ERROR.counter.increment() DeliveryStatus(RestStatus.FORBIDDEN.name, "Feature is not enabled for channel") } else { null @@ -300,6 +308,7 @@ object SendMessageActionHelper { eventStatus: EventStatus, referenceId: String ): EventStatus { + Metrics.NOTIFICATIONS_MESSAGE_DESTINATION_SLACK.counter.increment() val destination = SlackDestination(slack.url) val status = sendMessageThroughSpi(destination, message, referenceId) return eventStatus.copy(deliveryStatus = DeliveryStatus(status.statusCode.toString(), status.statusText)) @@ -314,6 +323,7 @@ object SendMessageActionHelper { eventStatus: EventStatus, referenceId: String ): EventStatus { + Metrics.NOTIFICATIONS_MESSAGE_DESTINATION_CHIME.counter.increment() val destination = ChimeDestination(chime.url) val status = sendMessageThroughSpi(destination, message, referenceId) return eventStatus.copy(deliveryStatus = DeliveryStatus(status.statusCode.toString(), status.statusText)) @@ -328,6 +338,7 @@ object SendMessageActionHelper { eventStatus: EventStatus, referenceId: String ): EventStatus { + Metrics.NOTIFICATIONS_MESSAGE_DESTINATION_WEBHOOK.counter.increment() val destination = CustomWebhookDestination(webhook.url, webhook.headerParams, webhook.method.tag) val status = sendMessageThroughSpi(destination, message, referenceId) return eventStatus.copy(deliveryStatus = DeliveryStatus(status.statusCode.toString(), status.statusText)) @@ -343,6 +354,7 @@ object SendMessageActionHelper { eventStatus: EventStatus, referenceId: String ): EventStatus { + Metrics.NOTIFICATIONS_MESSAGE_DESTINATION_EMAIL.counter.increment() val accountDocInfo = childConfigs.find { it.docInfo.id == email.emailAccountID } val groups = childConfigs.filter { email.emailGroupIds.contains(it.docInfo.id) } val groupRecipients = groups.map { (it.configDoc.config.configData as EmailGroup).recipients }.flatten() @@ -403,6 +415,7 @@ object SendMessageActionHelper { message: MessageContent, referenceId: String ): EmailRecipientStatus { + Metrics.NOTIFICATIONS_MESSAGE_DESTINATION_SMTP_ACCOUNT.counter.increment() val destination = SmtpDestination( accountName, smtpAccount.host, @@ -428,6 +441,7 @@ object SendMessageActionHelper { message: MessageContent, referenceId: String ): EmailRecipientStatus { + Metrics.NOTIFICATIONS_MESSAGE_DESTINATION_SES_ACCOUNT.counter.increment() val destination = SesDestination( accountName, sesAccount.awsRegion, @@ -451,6 +465,7 @@ object SendMessageActionHelper { eventStatus: EventStatus, referenceId: String ): EventStatus { + Metrics.NOTIFICATIONS_MESSAGE_DESTINATION_SNS.counter.increment() val destination = SnsDestination(sns.topicArn, sns.roleArn) val status = sendMessageThroughSpi(destination, message, referenceId) return eventStatus.copy(deliveryStatus = DeliveryStatus(status.statusCode.toString(), status.statusText)) diff --git a/notifications/notifications/src/test/java/org/opensearch/notifications/metrics/BasicCounterTest.java b/notifications/notifications/src/test/java/org/opensearch/notifications/metrics/BasicCounterTest.java new file mode 100644 index 00000000..97bcc24b --- /dev/null +++ b/notifications/notifications/src/test/java/org/opensearch/notifications/metrics/BasicCounterTest.java @@ -0,0 +1,39 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.metrics; + +import org.junit.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +public class BasicCounterTest { + + @Test + public void increment() { + BasicCounter counter = new BasicCounter(); + for (int i=0; i<5; ++i) { + counter.increment(); + } + + assertThat(counter.getValue(), equalTo(5L)); + } + + @Test + public void incrementN() { + BasicCounter counter = new BasicCounter(); + counter.add(5); + + assertThat(counter.getValue(), equalTo(5L)); + } + +} \ No newline at end of file diff --git a/notifications/notifications/src/test/java/org/opensearch/notifications/metrics/RollingCounterTest.java b/notifications/notifications/src/test/java/org/opensearch/notifications/metrics/RollingCounterTest.java new file mode 100644 index 00000000..0e2330a4 --- /dev/null +++ b/notifications/notifications/src/test/java/org/opensearch/notifications/metrics/RollingCounterTest.java @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.notifications.metrics; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.time.Clock; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class RollingCounterTest { + + @Mock + Clock clock; + + @Test + public void increment() { + RollingCounter counter = new RollingCounter(3, 1, clock); + for (int i=0; i<5; ++i) { + counter.increment(); + } + + assertThat(counter.getValue(), equalTo(0L)); + + when(clock.millis()).thenReturn(1000L); // 1 second passed + assertThat(counter.getValue(), equalTo(5L)); + + counter.increment(); + counter.increment(); + + when(clock.millis()).thenReturn(2000L); // 1 second passed + assertThat(counter.getValue(), lessThanOrEqualTo(3L)); + + when(clock.millis()).thenReturn(3000L); // 1 second passed + assertThat(counter.getValue(), equalTo(0L)); + + } + + @Test + public void add() { + RollingCounter counter = new RollingCounter(3, 1, clock); + + counter.add(6); + assertThat(counter.getValue(), equalTo(0L)); + + when(clock.millis()).thenReturn(1000L); // 1 second passed + assertThat(counter.getValue(), equalTo(6L)); + + counter.add(4); + when(clock.millis()).thenReturn(2000L); // 1 second passed + assertThat(counter.getValue(), equalTo(4L)); + + when(clock.millis()).thenReturn(3000L); // 1 second passed + assertThat(counter.getValue(), equalTo(0L)); + } + + @Test + public void trim() { + RollingCounter counter = new RollingCounter(2, 1, clock); + + for (int i=1; i<6; ++i) { + counter.increment(); + assertThat(counter.size(), equalTo(i)); + when(clock.millis()).thenReturn(i * 1000L); // i seconds passed + } + counter.increment(); + assertThat(counter.size(), lessThanOrEqualTo(3)); + } +} From 7e1e4580457a4270a8959453806228f1a69f88f3 Mon Sep 17 00:00:00 2001 From: Joshua Date: Wed, 18 Aug 2021 15:42:50 -0700 Subject: [PATCH 23/29] Add ses_account to feature api response (#290) Signed-off-by: Joshua Li --- .../org/opensearch/integtest/features/GetPluginFeaturesIT.kt | 2 +- .../org/opensearch/notifications/spi/setting/PluginSettings.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/features/GetPluginFeaturesIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/features/GetPluginFeaturesIT.kt index 958edeb0..a0946bd9 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/features/GetPluginFeaturesIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/features/GetPluginFeaturesIT.kt @@ -42,7 +42,7 @@ class GetPluginFeaturesIT : PluginRestTestCase() { val configTypes = getResponse.get("config_type_list").asJsonArray.map { it.asString } if (configTypes.contains(ConfigType.EMAIL.tag)) { Assert.assertTrue(configTypes.contains(ConfigType.EMAIL_GROUP.tag)) - Assert.assertTrue(configTypes.contains(ConfigType.SMTP_ACCOUNT.tag)) + Assert.assertTrue(configTypes.contains(ConfigType.SMTP_ACCOUNT.tag) || configTypes.contains(ConfigType.SES_ACCOUNT.tag)) } } } diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/setting/PluginSettings.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/setting/PluginSettings.kt index 3a0c8cdf..23101d46 100644 --- a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/setting/PluginSettings.kt +++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/setting/PluginSettings.kt @@ -136,6 +136,7 @@ internal object PluginSettings { "webhook", "email", "sns", + "ses_account", "smtp_account", "email_group" ) From 52714e27f0f5554327c9b10213b49fc66bf68179 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Aug 2021 10:03:00 -0700 Subject: [PATCH 24/29] Bump hosted-git-info from 2.8.8 to 2.8.9 in /dashboards-notifications (#295) Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9. - [Release notes](https://github.com/npm/hosted-git-info/releases) - [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md) - [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9) --- updated-dependencies: - dependency-name: hosted-git-info dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dashboards-notifications/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dashboards-notifications/yarn.lock b/dashboards-notifications/yarn.lock index 685c9327..a5ab9eb4 100644 --- a/dashboards-notifications/yarn.lock +++ b/dashboards-notifications/yarn.lock @@ -2137,9 +2137,9 @@ has@^1.0.3: function-bind "^1.1.1" hosted-git-info@^2.1.4: - version "2.8.8" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" - integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== html-encoding-sniffer@^2.0.1: version "2.0.1" From 62577de2492d4a8ef1845dec4886fcae55d0257c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Aug 2021 10:03:23 -0700 Subject: [PATCH 25/29] Bump path-parse from 1.0.6 to 1.0.7 in /dashboards-notifications (#294) Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7. - [Release notes](https://github.com/jbgutierrez/path-parse/releases) - [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7) --- updated-dependencies: - dependency-name: path-parse dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dashboards-notifications/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dashboards-notifications/yarn.lock b/dashboards-notifications/yarn.lock index a5ab9eb4..9fc3ab1a 100644 --- a/dashboards-notifications/yarn.lock +++ b/dashboards-notifications/yarn.lock @@ -3591,9 +3591,9 @@ path-key@^3.0.0, path-key@^3.1.0: integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== pend@~1.2.0: version "1.2.0" From 84ca87065bbcda20adbfa46944f7baa821a16d0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Aug 2021 10:04:03 -0700 Subject: [PATCH 26/29] Bump ws from 7.4.4 to 7.5.3 in /dashboards-notifications (#293) Bumps [ws](https://github.com/websockets/ws) from 7.4.4 to 7.5.3. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/7.4.4...7.5.3) --- updated-dependencies: - dependency-name: ws dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dashboards-notifications/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dashboards-notifications/yarn.lock b/dashboards-notifications/yarn.lock index 9fc3ab1a..20177168 100644 --- a/dashboards-notifications/yarn.lock +++ b/dashboards-notifications/yarn.lock @@ -4717,9 +4717,9 @@ write-file-atomic@^3.0.0: typedarray-to-buffer "^3.1.5" ws@^7.4.4: - version "7.4.4" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" - integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== + version "7.5.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" + integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== xml-name-validator@^3.0.0: version "3.0.0" From a0ba53b0b640f643a43f1e900d996cb02030bc3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Aug 2021 10:04:32 -0700 Subject: [PATCH 27/29] Bump browserslist from 4.16.3 to 4.16.8 in /dashboards-notifications (#292) Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.16.3 to 4.16.8. - [Release notes](https://github.com/browserslist/browserslist/releases) - [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md) - [Commits](https://github.com/browserslist/browserslist/compare/4.16.3...4.16.8) --- updated-dependencies: - dependency-name: browserslist dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dashboards-notifications/yarn.lock | 46 +++++++++++++++--------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/dashboards-notifications/yarn.lock b/dashboards-notifications/yarn.lock index 20177168..1d8bf990 100644 --- a/dashboards-notifications/yarn.lock +++ b/dashboards-notifications/yarn.lock @@ -1038,15 +1038,15 @@ browser-process-hrtime@^1.0.0: integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== browserslist@^4.14.5: - version "4.16.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717" - integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw== + version "4.16.8" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.8.tgz#cb868b0b554f137ba6e33de0ecff2eda403c4fb0" + integrity sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ== dependencies: - caniuse-lite "^1.0.30001181" - colorette "^1.2.1" - electron-to-chromium "^1.3.649" + caniuse-lite "^1.0.30001251" + colorette "^1.3.0" + electron-to-chromium "^1.3.811" escalade "^3.1.1" - node-releases "^1.1.70" + node-releases "^1.1.75" bser@2.1.1: version "2.1.1" @@ -1108,10 +1108,10 @@ camelcase@^6.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== -caniuse-lite@^1.0.30001181: - version "1.0.30001199" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001199.tgz#062afccaad21023e2e647d767bac4274b8b8fd7f" - integrity sha512-ifbK2eChUCFUwGhlEzIoVwzFt1+iriSjyKKFYNfv6hN34483wyWpLLavYQXhnR036LhkdUYaSDpHg1El++VgHQ== +caniuse-lite@^1.0.30001251: + version "1.0.30001251" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz#6853a606ec50893115db660f82c094d18f096d85" + integrity sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A== capture-exit@^2.0.0: version "2.0.0" @@ -1279,10 +1279,10 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -colorette@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" - integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== +colorette@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.3.0.tgz#ff45d2f0edb244069d3b772adeb04fed38d0a0af" + integrity sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w== colors@^1.1.2: version "1.4.0" @@ -1555,10 +1555,10 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -electron-to-chromium@^1.3.649: - version "1.3.687" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.687.tgz#c336184b7ab70427ffe2ee79eaeaedbc1ad8c374" - integrity sha512-IpzksdQNl3wdgkzf7dnA7/v10w0Utf1dF2L+B4+gKrloBrxCut+au+kky3PYvle3RMdSxZP+UiCZtLbcYRxSNQ== +electron-to-chromium@^1.3.811: + version "1.3.812" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.812.tgz#4c4fb407e0e1335056097f172e9f2c0a09efe77d" + integrity sha512-7KiUHsKAWtSrjVoTSzxQ0nPLr/a+qoxNZwkwd9LkylTOgOXSVXkQbpIVT0WAUQcI5gXq3SwOTCrK+WfINHOXQg== elegant-spinner@^1.0.1: version "1.0.1" @@ -3331,10 +3331,10 @@ node-notifier@^8.0.0: uuid "^8.3.0" which "^2.0.2" -node-releases@^1.1.70: - version "1.1.71" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" - integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== +node-releases@^1.1.75: + version "1.1.75" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.75.tgz#6dd8c876b9897a1b8e5a02de26afa79bb54ebbfe" + integrity sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw== normalize-package-data@^2.5.0: version "2.5.0" From 68bd46f98e26993b7a2128222cfd1c6a93dc05e0 Mon Sep 17 00:00:00 2001 From: Joshua Date: Fri, 20 Aug 2021 13:31:03 -0700 Subject: [PATCH 28/29] Remove unused mock data (#297) Signed-off-by: Joshua Li --- .../containers/Notifications.tsx | 8 +- .../public/services/EventService.ts | 4 +- .../public/services/mockData.ts | 335 ------------------ 3 files changed, 6 insertions(+), 341 deletions(-) delete mode 100644 dashboards-notifications/public/services/mockData.ts diff --git a/dashboards-notifications/public/pages/Notifications/containers/Notifications.tsx b/dashboards-notifications/public/pages/Notifications/containers/Notifications.tsx index 963595c7..9dbf3293 100644 --- a/dashboards-notifications/public/pages/Notifications/containers/Notifications.tsx +++ b/dashboards-notifications/public/pages/Notifications/containers/Notifications.tsx @@ -198,13 +198,13 @@ export default class Notifications extends Component< const getNotificationsResponse = await services.eventService.getNotifications( queryObject ); - const getHistogramResponse = await services.eventService.getHistogram( - queryObject - ); + // const getHistogramResponse = await services.eventService.getHistogram( + // queryObject + // ); this.setState({ items: getNotificationsResponse.items, total: getNotificationsResponse.total, - histogramData: getHistogramResponse, + // histogramData: getHistogramResponse, }); } catch (err) { this.context.notifications.toasts.addDanger( diff --git a/dashboards-notifications/public/services/EventService.ts b/dashboards-notifications/public/services/EventService.ts index 8bcfe998..2b858ba1 100644 --- a/dashboards-notifications/public/services/EventService.ts +++ b/dashboards-notifications/public/services/EventService.ts @@ -27,7 +27,6 @@ import { HttpFetchQuery, HttpSetup } from '../../../../src/core/public'; import { NODE_API } from '../../common'; import { NOTIFICATION_SOURCE } from '../utils/constants'; -import { MOCK_GET_HISTOGRAM } from './mockData'; import { eventListToNotifications, eventToNotification } from './utils/helper'; interface EventsResponse { @@ -43,7 +42,8 @@ export default class EventService { } getHistogram = async (queryObject: object) => { - return MOCK_GET_HISTOGRAM(); + // TODO needs backend support + // return MOCK_GET_HISTOGRAM(); }; getNotifications = async (queryObject: HttpFetchQuery) => { diff --git a/dashboards-notifications/public/services/mockData.ts b/dashboards-notifications/public/services/mockData.ts deleted file mode 100644 index f7c46860..00000000 --- a/dashboards-notifications/public/services/mockData.ts +++ /dev/null @@ -1,335 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -import { DataGenerator } from '@elastic/charts'; - -export const MOCK_GET_HISTOGRAM = () => { - const dg = new DataGenerator(); - const n = 10; - const data = dg.generateGroupedSeries(26, n, 'Channel-'); - data[18].y = 20; - data[18 + 26 * 2].y = 30; - for (let channel = 0; channel < n; channel++) { - for (let index = 0; index < data.length / n; index++) { - const i = index + (channel * data.length) / n; - const element = data[i]; - element.y = Math.round(element.y); - element.x = 1618951331 + index * 1000 * 60 * 60; - } - } - return data; -}; - -export const MOCK_RECIPIENT_GROUPS = [ - { - id: '0', - name: 'admin_list', - email: Array.from({ length: 8 }, (v, i) => ({ - email: 'no-reply@company.com', - })), - description: 'Description about this group', - }, - { - id: '1', - name: 'on_call_list', - email: Array.from({ length: 2 }, (v, i) => ({ - email: 'no-reply@company.com', - })), - description: 'Description about this group', - }, - { - id: '2', - name: 'Team2', - email: Array.from({ length: 10 }, (v, i) => ({ - email: 'no-reply@company.com', - })), - description: 'Description about this group', - }, - { - id: '3', - name: 'Security_alerts', - email: Array.from({ length: 5 }, (v, i) => ({ - email: 'no-reply@company.com', - })), - description: 'Description about this group', - }, -]; - -export const MOCK_SENDERS = [ - { - id: '0', - name: 'Main', - from: 'no-reply@company.com', - host: 'smtp.company.com', - port: '80', - method: 'SSL', - }, - { - id: '1', - name: 'Reports', - from: 'reports@company.com', - host: 'smtp.company.com', - port: '80', - method: 'SSL', - }, - { - id: '2', - name: 'Admin bot', - from: 'admin_bot@company.com', - host: 'smtp-internal.company.com', - port: '80', - method: 'SSL', - }, - { - id: '3', - name: 'Alerting bot', - from: 'alerts@company.com', - host: 'smtp.company.com', - port: '80', - method: 'SSL', - }, -]; - -export const MOCK_CHANNELS = [ - { - id: '0', - name: 'Ops_channel', - enabled: true, - type: 'SLACK', - allowedFeatures: ['ALERTING'], - description: 'Notifies all full-time operational team members.', - lastUpdatedTime: 1618951331, - destination: { - slack: { - url: - 'https://hooks.slack.com/services/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', - }, - }, - }, - { - id: '1', - name: 'Team2', - enabled: true, - type: 'CHIME', - allowedFeatures: ['ALERTING', 'REPORTING'], - description: 'Notifies all full-time operational team members.', - lastUpdatedTime: 1618951331, - destination: { - chime: { - url: 'https://hooks.chime.com/example/url', - }, - }, - }, - { - id: '2', - name: 'Security_alerts', - enabled: true, - type: 'CUSTOM_WEBHOOK', - allowedFeatures: ['ISM', 'REPORTING'], - description: 'Notifies all full-time operational team members.', - lastUpdatedTime: 1618951331, - destination: { - custom_webhook: { - host: 'https://hooks.myhost.com', - port: 21, - path: 'custompath', - parameters: { - Parameter1: 'value1', - Parameter2: 'value2', - Parameter3: 'value3', - Parameter4: 'value4', - Parameter5: 'value5', - Parameter6: 'value6', - Parameter7: 'value7', - Parameter8: 'value8', - }, - headers: { - 'Content-Type': 'application/JSON', - 'WWW-Authenticate': - 'Basic realm="Access to the staging site", charset="UTF-8"', - 'Access-Control-Allow-Headers': - 'X-Custom-Header, Upgrade-Insecure-Requests, Accept, Cross-Origin-Embedder-Policy: require-corp Cross-Origin-Opener-Policy: same-origin', - Header1: 'value1', - Header2: 'value2', - Header3: 'value3', - Header4: 'value4', - Header5: 'value5', - Header6: 'value6', - Header7: 'value7', - Header8: 'value8', - }, - }, - }, - }, - { - id: '3', - name: 'Reporting_bot', - enabled: false, - type: 'EMAIL', - allowedFeatures: ['REPORTING'], - description: 'Notifies all full-time operational team members.', - lastUpdatedTime: 1618951331, - destination: { - email: { - email_account_id: 'robot@gmail.com', - header: '# sample header', - recipients: [ - 'Team 2', - 'cyberadmin@company.com', - 'Ops_team_weekly', - 'security_pos@company.com', - 'Team 5', - 'bot@company.com', - 'Team 7', - ], - }, - }, - }, - { - id: '4', - name: 'SNS channel test', - enabled: false, - type: 'SNS', - allowedFeatures: ['ISM'], - description: 'Notifies all full-time operational team members.', - lastUpdatedTime: 1618951331, - destination: { - sns: { - topic_arn: 'arn:aws:sns:us-east-1:24586493349034:es-alerting-test', // sns arn - role_arn: 'arn:aws:sns:us-east-1:24586493349034:es-alerting-test', // iam arn - }, - }, - }, - { - id: '5', - name: 'SES channel test', - enabled: false, - type: 'SES', - allowedFeatures: ['ALERTING', 'REPORTING'], - description: 'Notifies all full-time operational team members.', - lastUpdatedTime: 1618951331, - destination: { - ses: { - email_account_id: 'robot@gmail.com', - header: '# sample header', - recipients: [ - 'Team 2', - 'cyberadmin@company.com', - 'Ops_team_weekly', - 'Team 5', - 'bot@company.com', - 'Team 7', - 'security_pos@company.com', - ], - }, - }, - }, -]; - -export const MOCK_NOTIFICATIONS = { - notifications: [ - { - id: '1', - title: 'Alert notification on high error rate', - referenceId: 'alert_id_1', - source: 'ALERTING', - severity: 'High', - lastUpdatedTime: 1612229000, - tags: ['optional string list'], - status: 'Error', - statusList: [ - { - configId: '1', - configName: 'dev_email_channel', - configType: 'Email', - emailRecipientStatus: [ - { - recipient: 'dd@amazon.com', - deliveryStatus: { - statusCode: '500', - statusText: 'Some error', - }, - }, - { - recipient: 'cc@amazon.com', - deliveryStatus: { - statusCode: '404', - statusText: 'invalid', - }, - }, - ], - deliveryStatus: { - // check this on each channel is enough - statusCode: '500', - statusText: - 'Unavailable to send message. Invalid SMTP configuration.', - }, - }, - { - configId: '2', - configName: 'manage_slack_channel', - configType: 'Slack', - deliveryStatus: { - statusCode: '200', - statusText: 'Success', - }, - }, - ], - }, - { - id: '2', - title: 'another notification', - referenceId: 'alert_id_2', - source: 'ALERTING', - severity: 'High', - lastUpdatedTime: 1612229000, - tags: ['optional string list'], - status: 'Success', - statusList: [ - { - configId: '1', - configName: 'dev_email_channel', - configType: 'Email', - emailRecipientStatus: [ - { - recipient: 'dd@amazon.com', - deliveryStatus: { - statusCode: '500', - statusText: 'Some error', - }, - }, - { - recipient: 'zhongnan@amazon.com', - deliveryStatus: { - statusCode: '404', - statusText: 'invalid', - }, - }, - ], - deliveryStatus: { - statusCode: '500', - statusText: 'Error', - }, - }, - { - configId: '2', - configName: 'manage_slack_channel', - configType: 'Slack', - deliveryStatus: { - statusCode: '200', - statusText: 'Success', - }, - }, - ], - }, - ], - totalNotifications: 6, -}; From d760a8496cc72c3b9f6cb90783dc058051ddf094 Mon Sep 17 00:00:00 2001 From: Kavitha Conjeevaram Mohan Date: Mon, 23 Aug 2021 10:45:36 -0700 Subject: [PATCH 29/29] Delete NotificationEventIndexTest.kt --- .../NotificationEventIndexTest.kt | 168 ------------------ 1 file changed, 168 deletions(-) delete mode 100644 notifications/notifications/src/test/kotlin/org/opensearch/notifications/NotificationEventIndexTest.kt diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/NotificationEventIndexTest.kt b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/NotificationEventIndexTest.kt deleted file mode 100644 index c56a7f4d..00000000 --- a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/NotificationEventIndexTest.kt +++ /dev/null @@ -1,168 +0,0 @@ -package org.opensearch.notifications.index - -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.whenever -import junit.framework.Assert.assertEquals -import org.apache.logging.log4j.Logger -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.mockito.Mockito -import org.mockito.Mockito.* -import org.opensearch.action.ActionFuture -import org.opensearch.action.admin.indices.create.CreateIndexResponse -import org.opensearch.action.get.GetRequest -import org.opensearch.action.get.GetResponse -import org.opensearch.action.support.master.AcknowledgedResponse -import org.opensearch.client.AdminClient -import org.opensearch.client.Client -import org.opensearch.client.IndicesAdminClient -import org.opensearch.cluster.ClusterState -import org.opensearch.cluster.routing.RoutingTable -import org.opensearch.cluster.service.ClusterService -import org.opensearch.common.collect.MapBuilder -import org.opensearch.common.settings.Setting -import org.opensearch.common.settings.Settings -import org.opensearch.common.unit.ByteSizeValue -import org.opensearch.common.util.concurrent.ThreadContext -import org.opensearch.common.util.concurrent.ThreadContext.StoredContext -//import org.opensearch.common.util.concurrent.ThreadContext.ThreadContextStruct -import org.opensearch.commons.notifications.model.* -import org.opensearch.http.HttpTransportSettings -import org.opensearch.notifications.model.DocInfo -import org.opensearch.notifications.model.DocMetadata -import org.opensearch.notifications.model.NotificationEventDoc -import org.opensearch.notifications.model.NotificationEventDocInfo -import org.opensearch.notifications.util.SecureIndexClient -import org.opensearch.threadpool.ThreadPool -import java.time.Instant -import java.util.stream.Collector - - -internal class NotificationEventIndexTest{ - - - private lateinit var client: Client - - //@Mock - private val INDEX_NAME = ".opensearch-notifications-event" - - - private lateinit var clusterService: ClusterService - - @BeforeEach - fun setUp() { - client = mock(Client::class.java,"client") - clusterService = mock(ClusterService::class.java, "clusterservice") - //val secureIndexClient = mock(SecureIndexClient::class.java) - //whenever(SecureIndexClient(client)).thenReturn(secureIndexClient) - NotificationEventIndex.initialize(client, clusterService) - } - - - @Test - fun `index operation to get single event` () { - val id = "index-1" - val docInfo = DocInfo("index-1", 1, 1, 1) - //val eventDoc = mock(NotificationEventDoc::class.java) - val lastUpdatedTimeMs = Instant.ofEpochMilli(Instant.now().toEpochMilli()) - val createdTimeMs = lastUpdatedTimeMs.minusSeconds(1000) - val metadata = DocMetadata( - lastUpdatedTimeMs, - createdTimeMs, - "tenant", - listOf("User:user", "Role:sample_role", "BERole:sample_backend_role") - ) - val sampleEventSource = EventSource( - "title", - "reference_id", - Feature.ALERTING, - tags = listOf("tag1", "tag2"), - severity = SeverityType.INFO - ) - val status = EventStatus( - "config_id", - "name", - ConfigType.CHIME, - deliveryStatus = DeliveryStatus("200", "success") - ) - val sampleEvent = NotificationEvent(sampleEventSource, listOf(status)) - val eventDoc = NotificationEventDoc(metadata, sampleEvent) - val expectedEventDocInfo = NotificationEventDocInfo(docInfo, eventDoc) - - // val getRequest = GetRequest(INDEX_NAME).id(id) - //val mockActionFuture:ActionFuture = mock(ActionFuture::class.java) as ActionFuture - //whenever(NotificationEventIndex.client.get(any())).thenReturn(mockActionFuture) - - //whenever(client.get(getRequest)).thenReturn(mockActionFuture) - val clusterState = mock(ClusterState::class.java) - - whenever(clusterService.state()).thenReturn(clusterState) - val mockRoutingTable = mock(RoutingTable::class.java) - val mockHasIndex = mockRoutingTable.hasIndex(INDEX_NAME) - - // print("has index value is $mockHasIndex") - - whenever(clusterState.routingTable).thenReturn(mockRoutingTable) - whenever(mockRoutingTable.hasIndex(INDEX_NAME)).thenReturn(mockHasIndex) - - //val actionFuture = NotificationEventIndex.client.admin().indices().create(request) - - - - val admin = mock(AdminClient::class.java) - val indices = mock(IndicesAdminClient::class.java) - val mockCreateClient:ActionFuture = mock(ActionFuture::class.java) as ActionFuture - - whenever(client.admin()).thenReturn(admin) - whenever(admin.indices()).thenReturn(indices) - whenever(indices.create(any())).thenReturn(mockCreateClient) - - //val time = PluginSettings.operationTimeoutMs - val mockActionGet = mock(CreateIndexResponse::class.java) - - // mockCreateClient.actionGet(PluginSettings.operationTimeoutMs) - whenever(mockCreateClient.actionGet(anyLong())).thenReturn(mockActionGet) - println("mockActionGet: $mockActionGet") - - //println("mockCreateClient: $mockCreateClient") - //println("plugin timout: $time") - - val mockResponse = mock(AcknowledgedResponse::class.java) - - //whenever(mockActionGet.isAcknowledged).thenReturn(mockResponse.isAcknowledged) - //whenever(mockActionGet.isAcknowledged).thenReturn(mockResponse) - //when(mockActionGet.isAcknowledged).thenReturn(true) - //doReturn(true).when(mockActionGet).isAcknowledged() - //Mockito.`when`(mockActionGet.isAcknowledged()).thenReturn(true) - - val getRequest = GetRequest(INDEX_NAME).id(id) - val mockActionFuture:ActionFuture = mock(ActionFuture::class.java) as ActionFuture - //whenever(client.get(any())).thenReturn(mockActionFuture) - - //client = mock(SecureIndexClient::class.java) - println("Mock action Future: $mockActionFuture") - whenever(client.get(getRequest)).thenReturn(mockActionFuture) - val mockThreadPool = mock(ThreadPool::class.java) - val mockThreadContext = mock(ThreadContext::class.java) - - whenever(client.threadPool()).thenReturn(mockThreadPool) - whenever(mockThreadPool.threadContext).thenReturn(mockThreadContext) - whenever(client.get(getRequest)).thenReturn(mockActionFuture) - - val actualEventDocInfo = NotificationEventIndex.getNotificationEvent(id) - //verify(clusterService.state(), atLeast(1)) - verify(mockCreateClient.actionGet(), atLeast(1)) - //verifyNoMoreInteractions() - - //val future = mock(client.admin().indices().create(request)) - /* - val mockFuture = mock(ActionFuture::class.java) - whenever(client.get(any())).thenReturn(mockFuture) - */ - - assertEquals(expectedEventDocInfo, actualEventDocInfo) - - } - -} -