- 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/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/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/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,
-};
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 };
diff --git a/dashboards-notifications/yarn.lock b/dashboards-notifications/yarn.lock
index 685c9327..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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
diff --git a/notifications/build.gradle b/notifications/build.gradle
index f0979a7f..b72992d8 100644
--- a/notifications/build.gradle
+++ b/notifications/build.gradle
@@ -28,9 +28,13 @@
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")
}
repositories {
@@ -54,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 888f419a..bec0f8e2 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')
}
@@ -71,17 +70,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
}
}
@@ -90,10 +78,8 @@ 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 ("software.amazon.awssdk:ses:2.16.75") {
- exclude module: 'annotations' // conflict with org.jetbrains:annotations, integTestRunner fails with error "codebase property already set"
- }
+ compile "${group}:common-utils:${common_utils_version}"
+ compile "org.json:json:20180813"
testImplementation(
'org.assertj:assertj-core:3.19.0',
@@ -116,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')
}
@@ -146,6 +132,11 @@ afterEvaluate {
}
test {
+ if (project.hasProperty('excludeTests')) {
+ project.properties['excludeTests']?.replaceAll('\\s', '')?.split('[,;]')?.each {
+ exclude "${it}"
+ }
+ }
systemProperty 'tests.security.manager', 'false'
useJUnitPlatform()
}
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..84c568f5 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,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.SendMessageAction
+import org.opensearch.notifications.action.PublishNotificationAction
import org.opensearch.notifications.action.SendNotificationAction
import org.opensearch.notifications.action.UpdateNotificationConfigAction
import org.opensearch.notifications.index.ConfigIndexingActions
@@ -61,12 +61,11 @@ 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.NotificationStatsRestHandler
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 +124,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 +133,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
@@ -167,6 +164,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
)
)
}
@@ -185,12 +186,12 @@ internal class NotificationPlugin : ActionPlugin, Plugin() {
): List {
log.debug("$LOG_PREFIX:getRestHandlers")
return listOf(
- SendMessageRestHandler(),
NotificationConfigRestHandler(),
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 f8f9d92a..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
@@ -31,18 +31,18 @@ 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.notifications.metrics.Metrics
import org.opensearch.tasks.Task
import org.opensearch.transport.TransportService
-import java.lang.IllegalArgumentException
/**
* Get feature channel list transport action
@@ -81,9 +81,10 @@ 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)) {
+ 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(
+ 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/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..e8435e16 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,23 +43,24 @@ 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.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
+import org.opensearch.notifications.metrics.Metrics
import org.opensearch.notifications.model.DocMetadata
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.
@@ -91,7 +92,12 @@ object ConfigIndexingActions {
// TODO: URL validation with rules
}
- private fun validateEmailConfig(email: Email, features: EnumSet, user: User?) {
+ @Suppress("UnusedPrivateMember")
+ private fun validateSnsConfig(sns: Sns, user: User?) {
+ // TODO: URL validation with rules
+ }
+
+ 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",
@@ -112,6 +118,7 @@ object ConfigIndexingActions {
when (it.configDoc.config.configType) {
ConfigType.EMAIL_GROUP -> 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
@@ -119,6 +126,15 @@ 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
+ )
+ }
+ 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
@@ -126,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
@@ -135,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
@@ -146,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
@@ -159,6 +178,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.
@@ -175,7 +199,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)
}
}
@@ -198,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)
}
@@ -218,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
@@ -226,14 +256,20 @@ 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
)
}
+ 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)) {
+ Metrics.NOTIFICATIONS_CONFIG_UPDATE_SYSTEM_ERROR.counter.increment()
throw OpenSearchStatusException("NotificationConfig Update failed", RestStatus.INTERNAL_SERVER_ERROR)
}
return UpdateNotificationConfigResponse(request.configId)
@@ -266,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(
@@ -294,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
@@ -302,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
@@ -347,7 +387,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)
@@ -370,7 +410,8 @@ object ConfigIndexingActions {
ConfigType.SLACK.tag,
ConfigType.CHIME.tag,
ConfigType.WEBHOOK.tag,
- ConfigType.EMAIL.tag
+ ConfigType.EMAIL.tag,
+ ConfigType.SNS.tag
)
}
@@ -386,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
@@ -394,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
@@ -421,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
@@ -429,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/ConfigQueryHelper.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigQueryHelper.kt
index 13df647b..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,13 +41,18 @@ 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
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
import org.opensearch.commons.notifications.model.ConfigType.WEBHOOK
import org.opensearch.index.query.BoolQueryBuilder
import org.opensearch.index.query.QueryBuilder
@@ -73,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,
@@ -84,7 +90,11 @@ 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_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/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 ec214bc0..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
@@ -143,6 +144,11 @@ 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
+ * 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]
@@ -185,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(
@@ -199,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()),
@@ -210,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)
@@ -261,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 f8e56907..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
@@ -15,8 +15,8 @@ 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.notifications.metrics.Metrics
import org.opensearch.rest.BaseRestHandler.RestChannelConsumer
import org.opensearch.rest.BytesRestResponse
import org.opensearch.rest.RestHandler.Route
@@ -71,7 +71,9 @@ 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))
+ 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(
client,
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/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/resthandler/SendTestMessageRestHandler.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/SendTestMessageRestHandler.kt
index 3795f73b..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
@@ -17,9 +17,9 @@ 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.notifications.metrics.Metrics
import org.opensearch.rest.BaseRestHandler.RestChannelConsumer
import org.opensearch.rest.BytesRestResponse
import org.opensearch.rest.RestHandler.Route
@@ -84,7 +84,9 @@ internal class SendTestMessageRestHandler : PluginBaseHandler() {
request: RestRequest,
client: NodeClient
) = RestChannelConsumer {
- val feature = Feature.fromTagOrDefault(request.param(FEATURE_TAG, Feature.NONE.tag))
+ 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)
val message = ChannelMessage(
@@ -102,7 +104,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 +113,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 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 d0efc5be..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
@@ -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
@@ -28,12 +35,16 @@ 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
import org.opensearch.commons.notifications.model.Webhook
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
@@ -44,7 +55,10 @@ 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
import org.opensearch.rest.RestStatus
import java.time.Instant
@@ -90,10 +104,26 @@ 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)
}
+ /**
+ * 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
@@ -151,36 +181,104 @@ 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()
+ 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)
- 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.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.SES_ACCOUNT -> null
ConfigType.SMTP_ACCOUNT -> null
ConfigType.EMAIL_GROUP -> 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}")
+ Metrics.NOTIFICATIONS_SEND_MESSAGE_USER_ERROR_NOT_FOUND.counter.increment()
eventStatus.copy(deliveryStatus = DeliveryStatus(RestStatus.NOT_FOUND.name, "Channel not found"))
} else {
response
}
}
+ /**
+ * 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
@@ -194,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
@@ -203,27 +302,45 @@ 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 {
+ Metrics.NOTIFICATIONS_MESSAGE_DESTINATION_SLACK.counter.increment()
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 {
+ Metrics.NOTIFICATIONS_MESSAGE_DESTINATION_CHIME.counter.increment()
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 {
- val destination = CustomWebhookDestination(webhook.url, webhook.headerParams, "POST")
- val status = sendMessageThroughSpi(destination, message)
+ private fun sendWebhookMessage(
+ webhook: Webhook,
+ message: MessageContent,
+ 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))
}
@@ -234,16 +351,40 @@ object SendMessageActionHelper {
email: Email,
childConfigs: List,
message: MessageContent,
- eventStatus: EventStatus
+ eventStatus: EventStatus,
+ referenceId: String
): EventStatus {
- val smtpAccount = childConfigs.find { it.docInfo.id == email.emailAccountID }
+ 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()
val recipients = email.recipients.union(groupRecipients)
val emailRecipientStatus: List
+ val accountConfig = accountDocInfo?.configDoc!!.config
runBlocking {
val statusDeferredList = recipients.map {
- async(Dispatchers.IO) { sendEmailFromSmtpAccount(smtpAccount, it, message) }
+ async(Dispatchers.IO) {
+ 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()
}
@@ -267,29 +408,80 @@ object SendMessageActionHelper {
/**
* send message to smtp destination
*/
- @Suppress("UnusedPrivateMember")
private fun sendEmailFromSmtpAccount(
- smtpAccount: NotificationConfigDocInfo?,
+ accountName: String,
+ smtpAccount: SmtpAccount,
recipient: String,
- message: MessageContent
+ message: MessageContent,
+ referenceId: String
): EmailRecipientStatus {
- // TODO implement email channel conversion
+ Metrics.NOTIFICATIONS_MESSAGE_DESTINATION_SMTP_ACCOUNT.counter.increment()
+ val destination = SmtpDestination(
+ accountName,
+ smtpAccount.host,
+ smtpAccount.port,
+ smtpAccount.method.tag,
+ smtpAccount.fromAddress,
+ recipient
+ )
+ val status = sendMessageThroughSpi(destination, message, referenceId)
return EmailRecipientStatus(
recipient,
- DeliveryStatus(RestStatus.NOT_IMPLEMENTED.name, "SMTP Channel not implemented")
+ DeliveryStatus(status.statusCode.toString(), status.statusText)
)
}
+ /**
+ * send message to ses destination
+ */
+ private fun sendEmailFromSesAccount(
+ accountName: String,
+ sesAccount: SesAccount,
+ recipient: String,
+ message: MessageContent,
+ referenceId: String
+ ): EmailRecipientStatus {
+ Metrics.NOTIFICATIONS_MESSAGE_DESTINATION_SES_ACCOUNT.counter.increment()
+ 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
+ */
+ private fun sendSNSMessage(
+ sns: Sns,
+ message: MessageContent,
+ 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))
+ }
+
/**
* Send message to destination using SPI
*/
@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/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..26eb0b68 100644
--- a/notifications/notifications/src/main/resources/notifications-config-mapping.yml
+++ b/notifications/notifications/src/main/resources/notifications-config-mapping.yml
@@ -100,6 +100,19 @@ properties:
type: keyword
email_group_id_list:
type: keyword
+ sns: # sns configuration
+ type: object
+ properties:
+ topic_arn:
+ type: text
+ fields:
+ keyword:
+ type: keyword
+ role_arn:
+ type: text
+ fields:
+ keyword:
+ type: keyword
smtp_account: # smtp account configuration
type: object
properties:
@@ -117,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/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));
+ }
+}
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/integtest/config/ChimeNotificationConfigCrudIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/ChimeNotificationConfigCrudIT.kt
index 40149453..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
)
@@ -203,4 +204,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,
+ setOf(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
+ )
+ }
}
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..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
@@ -28,12 +28,14 @@
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.SesAccount
import org.opensearch.commons.notifications.model.SmtpAccount
import org.opensearch.integtest.PluginRestTestCase
import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI
@@ -43,11 +45,10 @@ 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() {
- 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",
@@ -59,7 +60,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 +101,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 +146,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 +242,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
)
@@ -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 = """
@@ -367,7 +643,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 +701,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 +759,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 +804,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 +894,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 +1157,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 +1202,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 e8c4fa9a..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,15 +28,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.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
@@ -50,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) }
@@ -104,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(
@@ -397,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)
@@ -572,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)
@@ -817,4 +819,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,
+ setOf(FEATURE_ALERTING, FEATURE_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/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/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/notifications/src/test/kotlin/org/opensearch/integtest/send/SendTestMessageRestHandlerIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/send/SendTestMessageRestHandlerIT.kt
index 22dd3f20..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
@@ -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,159 @@ internal class SendTestMessageRestHandlerIT : PluginRestTestCase() {
Assert.assertNotNull(getResponseItem.get("event").asJsonObject)
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(
+ "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)
+ }
}
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 5f315be7..00000000
--- a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/NotificationEventIndexTest.kt
+++ /dev/null
@@ -1,134 +0,0 @@
-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 junit.framework.Assert.assertEquals
-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.GetResponse
-import org.opensearch.action.support.master.AcknowledgedResponse
-import org.opensearch.client.AdminClient
-import org.opensearch.client.IndicesAdminClient
-import org.opensearch.cluster.ClusterState
-import org.opensearch.cluster.routing.RoutingTable
-import org.opensearch.commons.notifications.model.*
-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 java.time.Instant
-
-
-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")
- 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 = 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(response.isAcknowledged).thenReturn(mockResponse)
-
- 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)
-
- }
-
-}
-
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..5735a186 100644
--- a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/ObjectEqualsHelpers.kt
+++ b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/ObjectEqualsHelpers.kt
@@ -33,11 +33,12 @@ 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.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) {
@@ -75,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)
@@ -82,7 +94,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)
@@ -92,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/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
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
)
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 68226dcf..bcff3811 100644
--- a/notifications/spi/build.gradle
+++ b/notifications/spi/build.gradle
@@ -89,12 +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 "org.apache.httpcomponents:httpclient:4.5.10" // resolve for awssdk:ses
- force "org.apache.httpcomponents:httpcore:4.4.13" // 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
}
}
@@ -104,12 +110,11 @@ 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"
- //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 "com.amazonaws:aws-java-sdk-sns:${aws_version}"
+ compile "com.amazonaws:aws-java-sdk-sts:${aws_version}"
+ 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(
'org.assertj:assertj-core:3.16.1',
'org.junit.jupiter:junit-jupiter-api:5.6.2',
@@ -143,7 +148,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/NotificationSpi.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/NotificationSpi.kt
index e3f9769c..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
@@ -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
@@ -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 = DestinationFactoryProvider.getFactory(destination.destinationType)
- destinationFactory.sendMessage(destination, message)
+ val destinationFactory = DestinationTransportProvider.getTransport(destination.destinationType)
+ destinationFactory.sendMessage(destination, message, referenceId)
} as PrivilegedAction?
)
}
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/DestinationClientPool.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationClientPool.kt
index a604cee1..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,10 +27,15 @@
package org.opensearch.notifications.spi.client
+import org.opensearch.notifications.spi.credentials.oss.SesClientFactoryImpl
+import org.opensearch.notifications.spi.credentials.oss.SnsClientFactoryImpl
+
/**
* This class provides Client to the relevant destinations
*/
internal object DestinationClientPool {
val httpClient: DestinationHttpClient = DestinationHttpClient()
- val emailClient: DestinationEmailClient = DestinationEmailClient()
+ val smtpClient: DestinationSmtpClient = DestinationSmtpClient()
+ 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 7ebeb85b..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
@@ -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
@@ -109,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)
@@ -127,7 +130,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 +145,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"
)
@@ -156,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)
@@ -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 CustomWebhook 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/client/DestinationSesClient.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSesClient.kt
new file mode 100644
index 00000000..819bc233
--- /dev/null
+++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSesClient.kt
@@ -0,0 +1,158 @@
+/*
+ * 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.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
+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 java.io.ByteArrayOutputStream
+import java.nio.ByteBuffer
+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 client = sesClientFactory.createSesClient(sesAwsRegion, roleArn)
+ val outputStream = ByteArrayOutputStream()
+ SecurityAccess.doPrivileged { mimeMessage.writeTo(outputStream) }
+ val emailSize = outputStream.size()
+ if (emailSize <= PluginSettings.emailSizeLimit) {
+ 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:${response.messageId}")
+ } 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: AmazonSimpleEmailServiceException) {
+ DestinationMessageResponse(RestStatus.FAILED_DEPENDENCY.status, getSesExceptionText(exception))
+ } catch (exception: AmazonServiceException) {
+ DestinationMessageResponse(RestStatus.FAILED_DEPENDENCY.status, getServiceExceptionText(exception))
+ } catch (exception: SdkBaseException) {
+ 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: AmazonSimpleEmailServiceException): String {
+ log.info("$LOG_PREFIX:SesException $exception")
+ 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}"
+ }
+
+ /**
+ * Create error string from Amazon SDK Exceptions
+ * @param exception SDK Exception
+ * @return generated error 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/client/DestinationEmailClient.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSmtpClient.kt
similarity index 52%
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..490146f8 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
@@ -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.destination.EmailDestination
+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
@@ -30,15 +34,19 @@ 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 {
- 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}"
@@ -47,32 +55,68 @@ class DestinationEmailClient {
val prop = Properties()
prop["mail.transport.protocol"] = "smtp"
- prop["mail.smtp.host"] = emailDestination.host
- prop["mail.smtp.port"] = emailDestination.port
- val session = Session.getInstance(prop)
+ prop["mail.smtp.host"] = smtpDestination.host
+ prop["mail.smtp.port"] = smtpDestination.port
+ var 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" -> {}
+ "none" -> {
+ }
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, emailDestination, 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
+ return if (emailUsername == null || emailPassword == null) {
+ null
+ } else {
+ SecureDestinationSettings(emailUsername, emailPassword)
+ }
}
/**
* {@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))
@@ -100,20 +144,4 @@ class DestinationEmailClient {
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
new file mode 100644
index 00000000..65ac3bcd
--- /dev/null
+++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/client/DestinationSnsClient.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.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(private val snsClientFactory: SnsClientFactory) {
+
+ 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 fdc3141b..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.EmailDestination
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 emailDestination
- * @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,
- emailDestination: EmailDestination,
+ fromAddress: String,
+ recipient: String,
messageContent: MessageContent
): MimeMessage {
// Create a new MimeMessage object
val mimeMessage = MimeMessage(session)
// Add from:
- mimeMessage.setFrom(emailDestination.fromAddress)
+ mimeMessage.setFrom(fromAddress)
// Add to:
- mimeMessage.setRecipients(Message.RecipientType.TO, emailDestination.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
new file mode 100644
index 00000000..7ebe5dc6
--- /dev/null
+++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/CredentialsProvider.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
+
+import com.amazonaws.auth.AWSCredentialsProvider
+
+/**
+ * AWS Credential provider using region and/or role
+ */
+interface CredentialsProvider {
+ /**
+ * 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..df437ff5
--- /dev/null
+++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SesClientFactory.kt
@@ -0,0 +1,20 @@
+/*
+ * 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.simpleemail.AmazonSimpleEmailService
+
+/**
+ * Interface for creating SES client
+ */
+interface SesClientFactory {
+ fun createSesClient(region: String, roleArn: String?): AmazonSimpleEmailService
+}
diff --git a/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SnsClientFactory.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SnsClientFactory.kt
new file mode 100644
index 00000000..0f2c7f15
--- /dev/null
+++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/SnsClientFactory.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 com.amazonaws.services.sns.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
new file mode 100644
index 00000000..fb06254f
--- /dev/null
+++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/CredentialsProviderFactory.kt
@@ -0,0 +1,50 @@
+/*
+ * 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
+
+class CredentialsProviderFactory : CredentialsProvider {
+ override fun getCredentialsProvider(region: String, roleArn: String?): AWSCredentialsProvider {
+ return if (roleArn != null) {
+ getCredentialsProviderByIAMRole(region, roleArn)
+ } else {
+ DefaultAWSCredentialsProviderChain()
+ }
+ }
+
+ private fun getCredentialsProviderByIAMRole(region: String, roleArn: String?): AWSCredentialsProvider {
+ // TODO cache credentials by role ARN?
+ val stsClient = AWSSecurityTokenServiceClientBuilder.standard()
+ .withCredentials(ProfileCredentialsProvider())
+ .withRegion(region)
+ .build()
+ val roleRequest = AssumeRoleRequest()
+ .withRoleArn(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/SesClientFactoryImpl.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/credentials/oss/SesClientFactoryImpl.kt
new file mode 100644
index 00000000..341b0e91
--- /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 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
+
+/**
+ * Factory for creating SES client
+ */
+object SesClientFactoryImpl : SesClientFactory {
+ override fun createSesClient(region: String, roleArn: String?): AmazonSimpleEmailService {
+ return SecurityAccess.doPrivileged {
+ 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/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/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/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/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/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/SesDestination.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SesDestination.kt
new file mode 100644
index 00000000..73c6aa41
--- /dev/null
+++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SesDestination.kt
@@ -0,0 +1,35 @@
+/*
+ * 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 com.amazonaws.regions.Regions
+import org.opensearch.common.Strings
+import org.opensearch.notifications.spi.utils.validateEmail
+
+/**
+ * This class holds the contents of ses destination
+ */
+class SesDestination(
+ val accountName: String,
+ 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(Regions.values().any { it.name == awsRegion }) { "aws region is not valid" }
+ validateEmail(fromAddress)
+ validateEmail(recipient)
+ }
+}
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..ac76bf71 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,21 @@ 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 accountName: String,
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/model/destination/SnsDestination.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SnsDestination.kt
new file mode 100644
index 00000000..19432b13
--- /dev/null
+++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/SnsDestination.kt
@@ -0,0 +1,22 @@
+/*
+ * 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) {
+ // 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/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..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
@@ -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.
*/
@@ -74,6 +82,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.
*/
@@ -117,10 +135,27 @@ internal object PluginSettings {
"chime",
"webhook",
"email",
+ "sns",
+ "ses_account",
"smtp_account",
"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
+
+ /**
+ * Default destination settings
+ */
+ private val DEFAULT_DESTINATION_SETTINGS = emptyMap()
+
/**
* list of allowed config types.
*/
@@ -163,6 +198,24 @@ internal object PluginSettings {
@Volatile
var socketTimeout: Int
+ /**
+ * Tooltip support
+ */
+ @Volatile
+ var tooltipSupport: Boolean
+
+ /**
+ * list of allowed config types.
+ */
+ @Volatile
+ var hostDenyList: List
+
+ /**
+ * Destination Settings
+ */
+ @Volatile
+ var destinationSettings: Map
+
private const val DECIMAL_RADIX: Int = 10
private val log by logger(javaClass)
@@ -190,6 +243,9 @@ 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
+ destinationSettings = if (settings != null) loadDestinationSettings(settings) else DEFAULT_DESTINATION_SETTINGS
defaultSettings = mapOf(
EMAIL_SIZE_LIMIT_KEY to emailSizeLimit.toString(DECIMAL_RADIX),
@@ -197,7 +253,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 +302,31 @@ 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
+ )
+
+ 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.
*
@@ -258,7 +340,11 @@ internal object PluginSettings {
MAX_CONNECTIONS_PER_ROUTE,
CONNECTION_TIMEOUT_MILLISECONDS,
SOCKET_TIMEOUT_MILLISECONDS,
- ALLOWED_CONFIG_TYPES
+ ALLOWED_CONFIG_TYPES,
+ TOOLTIP_SUPPORT,
+ HOST_DENY_LIST,
+ EMAIL_USERNAME,
+ EMAIL_PASSWORD
)
}
/**
@@ -273,6 +359,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 +399,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 +454,46 @@ 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")
+ }
+ }
+
+ 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/main/kotlin/org/opensearch/notifications/spi/factory/DestinationFactory.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransport.kt
similarity index 75%
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..5ca1ebe2 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,15 +20,18 @@ 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.
*
+ * @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
new file mode 100644
index 00000000..b1c538a1
--- /dev/null
+++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/DestinationTransportProvider.kt
@@ -0,0 +1,50 @@
+/*
+ * 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()
+ private val snsDestinationTransport = SnsDestinationTransport()
+ private val sesDestinationTransport = SesDestinationTransport()
+
+ @OpenForTesting
+ var destinationTransportMap = mapOf(
+ DestinationType.SLACK to webhookDestinationTransport,
+ DestinationType.CHIME to webhookDestinationTransport,
+ DestinationType.CUSTOM_WEBHOOK to webhookDestinationTransport,
+ DestinationType.SMTP to smtpDestinationTransport,
+ DestinationType.SNS to snsDestinationTransport,
+ DestinationType.SES to sesDestinationTransport
+ )
+
+ /**
+ * 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/SesDestinationTransport.kt
similarity index 73%
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/SesDestinationTransport.kt
index b0eec98d..cc891d11 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/SesDestinationTransport.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.DestinationSesClient
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.SesDestination
import org.opensearch.notifications.spi.utils.OpenForTesting
import org.opensearch.notifications.spi.utils.logger
import org.opensearch.rest.RestStatus
@@ -26,23 +26,27 @@ 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 SesDestinationTransport : DestinationTransport {
- private val log by logger(SmtpEmailDestinationFactory::class.java)
- private val destinationEmailClient: DestinationEmailClient
+ private val log by logger(SesDestinationTransport::class.java)
+ private val destinationEmailClient: DestinationSesClient
constructor() {
- this.destinationEmailClient = DestinationClientPool.emailClient
+ this.destinationEmailClient = DestinationClientPool.sesClient
}
@OpenForTesting
- constructor(destinationEmailClient: DestinationEmailClient) {
- this.destinationEmailClient = destinationEmailClient
+ constructor(destinationSesClient: DestinationSesClient) {
+ this.destinationEmailClient = destinationSesClient
}
- override fun sendMessage(destination: EmailDestination, message: MessageContent): DestinationMessageResponse {
+ override fun sendMessage(
+ destination: SesDestination,
+ 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 SmtpEmailDestinationFactory : DestinationFactory {
+
+ private val log by logger(SmtpDestinationTransport::class.java)
+ private val destinationEmailClient: DestinationSmtpClient
+
+ constructor() {
+ this.destinationEmailClient = DestinationClientPool.smtpClient
+ }
+
+ @OpenForTesting
+ constructor(destinationSmtpClient: DestinationSmtpClient) {
+ this.destinationEmailClient = destinationSmtpClient
+ }
+
+ override fun sendMessage(
+ destination: SmtpDestination,
+ 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/SnsDestinationTransport.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SnsDestinationTransport.kt
new file mode 100644
index 00000000..5cffeb91
--- /dev/null
+++ b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/SnsDestinationTransport.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.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.utils.OpenForTesting
+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)
+ private val destinationSNSClient: DestinationSnsClient
+
+ 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 response = destinationSNSClient.execute(destination, message, referenceId)
+ DestinationMessageResponse(RestStatus.OK.status, "Success, message id: $response")
+ } 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/factory/WebhookDestinationFactory.kt b/notifications/spi/src/main/kotlin/org/opensearch/notifications/spi/transport/WebhookDestinationTransport.kt
similarity index 77%
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..4225065c 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() {
@@ -38,12 +38,16 @@ internal class WebhookDestinationFactory : DestinationFactory) {
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,8 +49,22 @@ 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
+ return ("https" == url.protocol || "http" == url.protocol) // Support only http/https, 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
}
/**
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..2eba8650 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,19 +33,20 @@ 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
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
@@ -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)
@@ -83,26 +83,25 @@ 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: " +
- "link test: http://sample.com email test: marymajor@example.com All member callout: " +
- "@All All Present member callout: @Present"
+ "link test: http://sample.com email test: marymajor@example.com All member call out: " +
+ "@All All Present member call out: @Present"
val url = "https://abc/com"
val destination = ChimeDestination(url)
val message = MessageContent(title, messageText)
- val actualChimeResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message)
+ val actualChimeResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message, "referenceId")
assertEquals(expectedWebhookResponse.statusText, actualChimeResponse.statusText)
assertEquals(expectedWebhookResponse.statusCode, actualChimeResponse.statusCode)
}
@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, "")
@@ -118,26 +117,25 @@ 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: " +
- "link test: http://sample.com email test: marymajor@example.com All member callout: " +
- "@All All Present member callout: @Present\"}"
+ "link test: http://sample.com email test: marymajor@example.com All member call out: " +
+ "@All All Present member call out: @Present\"}"
val url = "https://abc/com"
val destination = ChimeDestination(url)
val message = MessageContent(title, messageText)
- val actualChimeResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message)
+ val actualChimeResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message, "referenceId")
assertEquals(expectedWebhookResponse.statusText, actualChimeResponse.statusText)
assertEquals(expectedWebhookResponse.statusCode, actualChimeResponse.statusCode)
}
@Test
- @Throws(Exception::class)
fun `test chime message non-empty entity response`() {
val responseContent = "It worked!"
val mockHttpClient: CloseableHttpClient = EasyMock.createMock(CloseableHttpClient::class.java)
@@ -154,32 +152,30 @@ 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: " +
- "link test: http://sample.com email test: marymajor@example.com All member callout: " +
- "@All All Present member callout: @Present\"}"
+ "link test: http://sample.com email test: marymajor@example.com All member call out: " +
+ "@All All Present member call out: @Present\"}"
val url = "https://abc/com"
val destination = ChimeDestination(url)
val message = MessageContent(title, messageText)
- val actualChimeResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message)
+ val actualChimeResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message, "referenceId")
assertEquals(expectedWebhookResponse.statusText, actualChimeResponse.statusText)
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..9361560d 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,19 +36,20 @@ 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
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
@@ -94,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"
@@ -108,7 +109,7 @@ internal class CustomWebhookDestinationTests {
val destination = CustomWebhookDestination(url, mapOf("headerKey" to "headerValue"), method)
val message = MessageContent(title, messageText)
- val actualCustomWebhookResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message)
+ val actualCustomWebhookResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message, "ref")
assertEquals(expectedWebhookResponse.statusText, actualCustomWebhookResponse.statusText)
assertEquals(expectedWebhookResponse.statusCode, actualCustomWebhookResponse.statusCode)
@@ -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, "")
@@ -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"
@@ -148,7 +148,7 @@ internal class CustomWebhookDestinationTests {
val destination = CustomWebhookDestination(url, mapOf("headerKey" to "headerValue"), method)
val message = MessageContent(title, messageText)
- val actualCustomWebhookResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message)
+ val actualCustomWebhookResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message, "ref")
assertEquals(expectedWebhookResponse.statusText, actualCustomWebhookResponse.statusText)
assertEquals(expectedWebhookResponse.statusCode, actualCustomWebhookResponse.statusCode)
@@ -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
@@ -177,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"
@@ -191,53 +190,55 @@ internal class CustomWebhookDestinationTests {
val destination = CustomWebhookDestination(url, mapOf("headerKey" to "headerValue"), method)
val message = MessageContent(title, messageText)
- val actualCustomWebhookResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message)
+ val actualCustomWebhookResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message, "ref")
assertEquals(expectedWebhookResponse.statusText, actualCustomWebhookResponse.statusText)
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/EmailDestinationTests.kt b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/EmailDestinationTests.kt
deleted file mode 100644
index 8e5f252f..00000000
--- a/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/EmailDestinationTests.kt
+++ /dev/null
@@ -1,120 +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
-
-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.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.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.rest.RestStatus
-import javax.mail.MessagingException
-
-@ExtendWith(MockitoExtension::class)
-internal class EmailDestinationTests {
-
- @Test
- @Throws(Exception::class)
- fun testSmtpEmailMessage() {
- val expectedEmailResponse = DestinationMessageResponse(RestStatus.OK.status, "Success")
- val emailClient = spyk()
- every { emailClient.sendMessage(any()) } returns Unit
-
- val smtpEmailDestinationFactory = SmtpEmailDestinationFactory(emailClient)
- DestinationFactoryProvider.destinationFactoryMap = mapOf(DestinationType.SMTP to smtpEmailDestinationFactory)
-
- 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 actualEmailResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message)
- assertEquals(expectedEmailResponse.statusCode, actualEmailResponse.statusCode)
- assertEquals(expectedEmailResponse.statusText, actualEmailResponse.statusText)
- }
-
- @Test
- @Throws(Exception::class)
- fun testSmtpFailingEmailMessage() {
- val expectedEmailResponse = DestinationMessageResponse(
- RestStatus.FAILED_DEPENDENCY.status,
- "Couldn't connect to host, port: localhost, 55555; timeout -1"
- )
- 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 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(
- "localhost",
- 55555,
- "none",
- "test@abc.com",
- "to@abc.com",
- DestinationType.SMTP
- )
-
- val actualEmailResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message)
-
- assertEquals(expectedEmailResponse.statusCode, actualEmailResponse.statusCode)
- assertEquals("sendEmail Error, status:${expectedEmailResponse.statusText}", actualEmailResponse.statusText)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun testHostMissingEmailDestination() {
- try {
- EmailDestination("", 465, "ssl", "from@test.com", "to@test.com", DestinationType.SMTP)
- } catch (exception: Exception) {
- Assert.assertEquals("Host name should be provided", exception.message)
- throw exception
- }
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun testInvalidPortEmailDestination() {
- try {
- EmailDestination("localhost", -1, "ssl", "from@test.com", "to@test.com", DestinationType.SMTP)
- } catch (exception: Exception) {
- Assert.assertEquals("Port should be positive value", exception.message)
- throw exception
- }
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun testMissingFromOrRecipientEmailDestination() {
- try {
- EmailDestination("localhost", 465, "ssl", "", "to@test.com", DestinationType.SMTP)
- } 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/SlackDestinationTests.kt b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/SlackDestinationTests.kt
index a6ff6a93..028e2f5c 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,20 +33,21 @@ 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
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
@@ -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)
@@ -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: " +
@@ -96,14 +96,13 @@ internal class SlackDestinationTests {
val destination = SlackDestination(url)
val message = MessageContent(title, messageText)
- val actualSlackResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message)
+ val actualSlackResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message, "ref")
assertEquals(expectedWebhookResponse.statusText, actualSlackResponse.statusText)
assertEquals(expectedWebhookResponse.statusCode, actualSlackResponse.statusCode)
}
@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, "")
@@ -119,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: " +
@@ -131,14 +130,13 @@ internal class SlackDestinationTests {
val destination = SlackDestination(url)
val message = MessageContent(title, messageText)
- val actualSlackResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message)
+ val actualSlackResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message, "ref")
assertEquals(expectedWebhookResponse.statusText, actualSlackResponse.statusText)
assertEquals(expectedWebhookResponse.statusCode, actualSlackResponse.statusCode)
}
@Test
- @Throws(Exception::class)
fun `test Slack message non-empty entity response`() {
val responseContent = "It worked!"
val mockHttpClient: CloseableHttpClient = EasyMock.createMock(CloseableHttpClient::class.java)
@@ -155,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: " +
@@ -167,20 +165,18 @@ internal class SlackDestinationTests {
val destination = SlackDestination(url)
val message = MessageContent(title, messageText)
- val actualSlackResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message)
+ val actualSlackResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message, "ref")
assertEquals(expectedWebhookResponse.statusText, actualSlackResponse.statusText)
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
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
new file mode 100644
index 00000000..f4470321
--- /dev/null
+++ b/notifications/spi/src/test/kotlin/org/opensearch/notifications/spi/SmtpDestinationTests.kt
@@ -0,0 +1,139 @@
+/*
+ * 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
+
+import io.mockk.every
+import io.mockk.spyk
+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
+import org.opensearch.notifications.spi.transport.SmtpDestinationTransport
+import org.opensearch.rest.RestStatus
+import javax.mail.MessagingException
+
+@ExtendWith(MockitoExtension::class)
+internal class SmtpDestinationTests {
+
+ @Test
+ fun testSmtpEmailMessage() {
+ val expectedEmailResponse = DestinationMessageResponse(RestStatus.OK.status, "Success")
+ val emailClient = spyk()
+ every { emailClient.sendMessage(any()) } returns Unit
+
+ 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 = SmtpDestination("testAccountName", "abc", 465, "ssl", "test@abc.com", "to@abc.com")
+
+ val actualEmailResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message, "referenceId")
+ 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, "referenceId")
+ assertEquals(expectedEmailResponse.statusCode, actualEmailResponse.statusCode)
+ assertEquals(expectedEmailResponse.statusText, actualEmailResponse.statusText)
+ }
+
+ @Test
+ fun testSmtpFailingEmailMessage() {
+ val expectedEmailResponse = DestinationMessageResponse(
+ RestStatus.FAILED_DEPENDENCY.status,
+ "Couldn't connect to host, port: localhost, 55555; timeout -1"
+ )
+ val emailClient = spyk()
+ every { emailClient.sendMessage(any()) } throws MessagingException(
+ "Couldn't connect to host, port: localhost, 55555; timeout -1"
+ )
+
+ 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 = SmtpDestination(
+ "testAccountName",
+ "localhost",
+ 55555,
+ "none",
+ "test@abc.com",
+ "to@abc.com"
+ )
+
+ val actualEmailResponse: DestinationMessageResponse = NotificationSpi.sendMessage(destination, message, "referenceId")
+
+ assertEquals(expectedEmailResponse.statusCode, actualEmailResponse.statusCode)
+ assertEquals("sendEmail Error, status:${expectedEmailResponse.statusText}", actualEmailResponse.statusText)
+ }
+
+ @Test
+ fun testHostMissingEmailDestination() {
+ 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
+ fun testInvalidPortEmailDestination() {
+ 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
+ fun testMissingFromOrRecipientEmailDestination() {
+ 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 4fa77e40..4ad2be5e 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,13 @@ internal class SmtpEmailIT : OpenSearchRestTestCase() {
}
fun `test send email to one recipient over smtp server`() {
- val emailDestination = EmailDestination(
+ val smtpDestination = SmtpDestination(
+ "testAccountName",
"localhost",
smtpPort,
"none",
"from@email.com",
- "test@localhost.com",
- DestinationType.SMTP
+ "test@localhost.com"
)
val message = MessageContent(
"Test smtp email title",
@@ -53,19 +52,19 @@ internal class SmtpEmailIT : OpenSearchRestTestCase() {
"VGVzdCBtZXNzYWdlCgo=",
"application/octet-stream",
)
- val response = NotificationSpi.sendMessage(emailDestination, message)
+ val response = NotificationSpi.sendMessage(smtpDestination, message, "ref")
assertEquals("Success", response.statusText)
assertEquals(RestStatus.OK.status, response.statusCode)
}
fun `test send email with non-available host`() {
- val emailDestination = EmailDestination(
+ val smtpDestination = SmtpDestination(
+ "testAccountName",
"invalidHost",
smtpPort,
"none",
"from@email.com",
- "test@localhost.com",
- DestinationType.SMTP
+ "test@localhost.com"
)
val message = MessageContent(
"Test smtp email title",
@@ -76,7 +75,7 @@ internal class SmtpEmailIT : OpenSearchRestTestCase() {
"VGVzdCBtZXNzYWdlCgo=",
"application/octet-stream",
)
- val response = NotificationSpi.sendMessage(emailDestination, message)
+ val response = NotificationSpi.sendMessage(smtpDestination, message, "ref")
assertEquals(
"sendEmail Error, status:Couldn't connect to host, port: invalidHost, $smtpPort; timeout -1",
response.statusText
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))
+ }
+ }
+}